GameWith Engineering Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

try! Swift Tokyo 2018 にスポンサーとして協賛します

GameWith は try! Swift Tokyo 2018 にゴールドスポンサーとして協賛します。

弊社の iOS アプリ開発は 2015 年のモンスト攻略アプリから始まりました。

直近では、ゲーム攻略情報や、同じゲームをプレイしているユーザー様同士でコミュニケーションを取るための SNS を提供する iOS アプリを 2017 年 11 月 にリリースしています。

またこの 2 年間で Swift はというと、最初のアプリ開発で使用しておりました Swift 2 から、現在は Swift 4 までバージョンアップされており、言語自体も年々洗練されてきていることを感じております。

今回は、このような素晴らしいプログラミング言語 Swift のコミュニティ「try! Swift」をさらに発展させていくお手伝いをしたいという思いから、イベントへのスポンサードを決定しました。

ゴールドスポンサーということで、当日は弊社もブース出展を行います。

今までは、あまりこういった機会がなかったため、積極的に皆様と関われると良いなと思っています。(そして、これからはこういった機会を増やしていきます!)

try! Swift に参加される方はぜひお気軽にお立ち寄りください。

イベント概要については、以下をご覧ください。

イベント概要

f:id:serimaryo:20171215145834p:plain

イベント名 : try! Swift Tokyo 2018

概要 : try! Swift は、 Swiftにおけるベストプラクティス、アプリ開発、サーバーサイドSwift、オープンソースSwift、そしてSwiftのコミュニティなど、プログラミング言語Swiftに関するコミュニティ主催のカンファレンスです。

日時 : 2018 年 3 月 1 日(木), 2 日(金)

会場 : ベルサール新宿グランド

公式 HP : https://www.tryswift.co/events/2018/tokyo/jp/

www.tryswift.co

通知ポップアップの中身をいい感じにスクロールできるようにする

GameWith のエンジニアの @terunuma です。gamewith.jp のフロントエンドに関わる部分を主に触っています。

gamewith.jp ではゲームアプリの攻略/紹介記事を掲載している他に、会員向けのコンテンツとしてアプリのリリース通知や Q&A や他のゲームユーザーと交流できる SNS/コミュニティ機能なども提供しています。

これらの会員向けのコンテンツではアプリリリースの通知や Q&A に質問/回答がついた、SNS でいいねされた時にユーザーに対して通知が飛ぶようになっており、ログイン中であれば通知ボタンにバッジが表示されます。通知ボタンをタップで内容も表示できるようになっています。

f:id:syque:20171213194748p:plain:w300

この通知ポップアップの中にユーザーに届いた通知一覧を表示しているのですが、ポップアップの表示範囲が狭いためスクロールができるようにしています。ですがモバイルだとスクロールの動きがカクカクしており触り心地がよくないので、CSS で -webkit-overflow-scrolling: touch; のプロパティを設定してスクロールがスムーズになるようにしました。

f:id:syque:20171213194531p:plain:w300

これで完成、となればよかったのですが実際に触ってみると通知をスクロールした後に画面が固まってしまうことがありました。原因を探ってみると、どうも通知一覧の上端か下端でスクロールしようとした時に body の方をスクロールしようとしていて、目には見えないけどバウンススクロール状態になっていてそれが終わるまでタッチ処理を受け付けない状態になっていたことが分かりました。

これを解決するために div の端でスクロールしても body をスクロールしないようにできないか、と思い調べてみたところ stackoverflow に Javascript を用いて div のスクロール位置を調整することで解決する方法が紹介されていました。

stackoverflow.com

仕組みとしてはこうです:

  • 対象の、スクロール可能な要素に touchstart のイベントをバインドする
  • そのイベント処理で、現在の div のスクロール位置が上端か下端かをチェックする
  • 上端であれば +1、下端であれば -1、スクロール位置を動かす
  • この状態で端まで引っ張ると、div 内部の要素で必ずバウンススクロールするので body がスクロールされない

実装したスクリプトは以下になります:

scrollableElement.on('touchstart', function() {
    // 上端なら +1 する
    if (scrollableElement.scrollTop() === 0) {
        scrollableElement.scrollTop(1);
    }

    // 下端なら +1 する
    // 要素の高さとスクロール位置の差から、現在位置が下端かどうかを計算する
    var currentScrollTopPosition = scrollableElement.scrollTop();
    var maxScrollTopPosition = scrollableElement[0].scrollHeight - scrollableElement[0].offsetHeight;

    if (currentScrollTopPosition >= maxScrollTopPosition) {
        scrollableElement.scrollTop(currentScrollTopPosition - 1);
    }
});

f:id:syque:20171213203628p:plain:w300

要素が +1/-1 スクロールされる動きは、通知ポップアップを注意深く見ながらタッチすれば見ることができますので興味のある方はぜひ観察してみてください。 (そしてニヤッとしてください 😎 )

これにより、通知ポップアップをスクロールしても画面が固まるという問題を解決することができました。 実際のところ、こういうハックをしなくても自然な実装で作れる(ex. CSS のみで完結できる)のが一番良いのですが、ユーザー体験が良くなる手段になるのであれば今後もコード的に無理のない範囲で取り入れていければと思います。

DroidKaigi 2018 にスポンサーとして協賛します

f:id:serimaryo:20171206151812p:plain

GameWith は DroidKaigi 2018 への「サポーターズ」スポンサーとして協賛を行います。

GameWith はウェブサイトだけでなく、より良いユーザ体験を実現するためにネイティブアプリの開発も行っています。

今回は、Android の技術コミュニティの発展のお手伝いを始めていきたいという思いから、スポンサーとして協賛させていただきました。

当日は弊社のエンジニアもイベントに参加する予定ですので、もし見かけましたらお気軽にお声がけください!

イベント概要については、以下をご覧ください。

イベント概要

イベント名 : DroidKaigi 2018

概要 : DroidKaigiはエンジニアが主役のAndroidカンファレンスです。 Android技術情報の共有とコミュニケーションを目的に、2018年2月8日(木)、9日(金)の2日間開催します。 今回は「ニッチな技術とコミュニケーション」を重視する予定です。

日時 : 2018 年 2 月 8 日(木), 9 日(金)

会場 : ベルサール新宿グランド コンファレンスセンター

主催 : DroidKaigi実行委員会

公式 HP : https://droidkaigi.jp/2018/

droidkaigi.jp

Spot Instance の価格を CloudWatch で記録する

はじめまして。GameWith のエンジニアの @serima です。 普段は GameWith というゲームメディアの機能開発やインフラに目を向けるお仕事をしています。

GameWith では EC2 インスタンスのスケールアウトを Auto Scaling を使用せずに独自のスクリプトでインスタンス数をコントロールしています。*1

それなりの数のインスタンス数を起動しようとしたときに、その Availability Zone におけるそのインスタンスタイプが枯渇しているという状態が一時的に存在し、目的のインスタンスが起動できなかった事がありました。*2

具体的には、以下のようなエラーメッセージが返されました。

InsufficientInstanceCapacity: We currently do not have sufficient c4.xlarge capacity in the Availability Zone you requested (ap-northeast-1a). Our system will be working on provisioning additional capacity. You can currently get c4.xlarge capacity by not specifying an Availability Zone in your request or choosing ap-northeast-1c. status code: 500, request id:

高負荷が予測されるタイミングでインスタンスを想定通り起動できていない状態になってしまうと、サービス運営に影響があるため、回避したい問題となります。

その Availability Zone における残りのインスタンス数などを取得する API などは用意されていないため、いかにその状態を予見するかという解決策を探りました。 何か手がかりになるものはないかとメトリクスを見ていると、枯渇していたタイミングの前には Spot Instance の価格が高騰しているという状態が観測できました。

EC2 のコンソールから、スポットリクエスト -> 価格設定履歴から以下のようなグラフを参照することができます。 f:id:serimaryo:20171124183654p:plain

これを監視し、高騰したタイミングでアラート通知できれば、インスタンス起動のタイミングを早くするなど別のオプションを選ぶことができるようになります。 GameWith では CloudWatch のアラームを PagerDuty に飛ばすような運用をしていますので、そのフローにのせられるとスムースです。

しかし、SpotInstance の価格はそのままでは CloudWatch のメトリクスとして設定できません。 そこで、AWS Lambda を利用して、CloudWatch にカスタムメトリクスとして 1 分おきに値を送信するようにしました。

具体的なスクリプトは以下です。(なお、ランタイムは Python 2.7 を使用しています。)

import boto3
import time
import os
from datetime import datetime, timedelta

region = os.getenv('AWS_REGION', 'ap-northeast-1')

ec2 = boto3.client('ec2', region_name=region)
cloudwatch = boto3.client('cloudwatch', region_name=region)

aws_availability_zone = 'ap-northeast-1a'

def put_cloudwatch(namespace, metric_name, instance_type, timestamp, value):
    metric_data = {
        'MetricName': metric_name,
        'Timestamp': timestamp,
        'Value': value,
        'Unit': 'None',
        'Dimensions': [
            {
                'Name': 'InstanceType',
                'Value': instance_type
            }
        ]
    }
    r = cloudwatch.put_metric_data(
        Namespace = namespace,
        MetricData = [metric_data]
    )
    return r

def lambda_handler(event, context):
    ec2_instance_types = [
        'c4.large',
        'c4.xlarge'
    ]

    metric_name = 'SpotInstancePrice'

    filters = [
        {'Name': 'availability-zone', 'Values': [aws_availability_zone]},
        {'Name': 'instance-type', 'Values': ec2_instance_types},
        {'Name': 'product-description', 'Values': ['Linux/UNIX']},
    ]

    now = datetime.now()
    one_min_ago = now - timedelta(minutes=1)

    spot_price_history = ec2.describe_spot_price_history(Filters=filters, StartTime=one_min_ago).get('SpotPriceHistory')

    for history in spot_price_history:
        instance_type = history.get('InstanceType')
        spot_price = float(history.get('SpotPrice'))
        put_cloudwatch('EC2', metric_name, instance_type, now, spot_price)

    return str(spot_price_history)

Lambda は、現在では定期実行できるようになっており、指定した時間間隔ごとにイベントを発火させることができます。 少し分かりづらいのですが、定期実行を設定する場合は、Lambda のコンソールではなく、CloudWatch のコンソールに移動し、サイドバーの「ルール」から設定することになります。

ちなみに、このスクリプトを処理するロールを新規で作成し、以下のようなポリシーをアタッチしました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:PutMetricData"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeSpotPriceHistory"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

カスタムメトリクスとして送信された値を元に、無事に以下のようなグラフを作成することができました。 今回は、On-Demand Price を一定時間超えていたらアラーム通知を行うように設定しました。

f:id:serimaryo:20171124180842p:plain

このような小ネタも引き続き、こちらのブログで公開していきたいと思います。

参考

*1:語ると長くなってしまうので、こちらについては後日別記事で書きたいと思います

*2:レイテンシの関係上、Single AZ で運用しています