くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログ。積極採用中です。

エンジニア紹介 Vol.1

f:id:curama-tech:20180706112744j:plain

こんにちは。ディレクターの塚本です。

今回は同じチームのエンジニアを紹介します!チームには5名のエンジニアがいて、日々アプリやWebの設計・開発を行っています。それではさっそくまいりましょう。

時々関西弁を話すネパール人、カーキ・スシャーン

  • <エンジニア歴>4年
  • <得意な言語>Typescript、 場合によってPython
  • <興味のある技術と理由>Full stack data science。Data scienceに興味があるけど、単純に誰からデータもらって分析だけじゃなくて、こんなデータがあれば面白いかもって思ったら自分でData engineeringできたら楽しそうだから。
  • <みんマの好きなところ>自由な感じが好き
  • <みんマに入社した理由>会社が好き + 日本で働いてみたかった

f:id:curama-tech:20180706112802j:plain

カーキさんは日本で育ったんじゃないかと思うほど流暢な日本語を話しますが、漢字は大嫌いで苦手です(今はわかりませんが数年前は弁当屋のメニューにある漢字が読めず、毎回唐揚げ弁当を注文していました)。また、誰に対しても優しいうえ、端正な顔立ちで会社の人気者です。

猫をこよなく愛する開発部門の父、のりすけ

  • <エンジニア歴>どこから数えればいいかわからなかったけど8年くらい
  • <好きな言語>Haskell, Typescript, Rust
  • <興味のある技術と理由>Haskell。値がなにかではなくこの型はなにが出来るかを考え、強力な型システムでやりたいことを抽象化し設計できる。
  • <みんマの好きなところ>猫がいる
  • <みんマに入社した理由>猫がいる
  • <みんマの最初の印象と実際>最初: 猫がいる、実際: 猫がいる

f:id:curama-tech:20180706112806j:plain

のりすけさんは開発に関する知識や経験が豊富で他のエンジニアからとても頼りにされています。仕事に関しては非常に知的で尊敬していますが、仕事以外の話になると途端にアホになることがあります。そのギャップがたまりません。

天才かよ、ヤン・ヤン

  • <エンジニア歴>5年くらい(大学院の時教授の会社でフルタイム2年半+日本で1年SE+みんマで2年未満)
  • <好きな言語>Kotlin
  • <興味のある技術と理由>最近はFlutterに気になります。エンジニアに対して、 Write once, run everywhere はいつも吸引力がありますから。
  • <エンジニアになった理由>金融系やSE管理の仕事もそれぞれ一年間試したことがありますが、やはりもの作りが一番楽しいと思います。
  • <みんマの好きなところ>エンジニアとして好きなところは三つがあって、自由と自由そして自由です。

f:id:curama-tech:20180706112818j:plain

ヤンさんは中国出身です。「次これをお願いしたいんですけど〜」と言うと、「もう終わってます」という会話が何度なされたことか。仕事が早い、かつ丁寧。天才かよ。

とにかく前へ!Duy 

  • <エンジニア歴>一年間ぐらいWebのエンジニアとして、プログラミングしています。一年半ほどSwiftでiOSのアプリを開発しています。
  • <好きな言語>pythonとSwiftです。
  • <興味のある技術と理由>データ分析と機械学習。データを分析して、機械をトレーニングして、ユーザーの行動を推測するのは引力があります。そして、実際にこの技術だとこれからたくさん分野に応用できると思います。
  • <エンジニアになった理由>技術のプロダクトとソフトウェアに興味があって、自分で技術のプロダクトを作りたいと思います。
  • <こんな人と働きたい>創造性がある人と技術のスキールを持っている人。

f:id:curama-tech:20180706112754j:plain

Duyくんはベトナム出身です。持ち前のコミュ力を活かして日本語も開発技術もぐんぐん吸収していきます。これまでDuyくんといっしょに仕事をする機会が多かったのですが、分からなくてもミスってもとにかく前へ!前へ!という気持ちをとても感じます。

困ったら笑う!Tuyen

  • <エンジニア歴>Webを開発(1年半)+みんま(1年半)
  • <好きな言語>Python, Typescript
  • <興味のある技術と理由>最近、設計システムに興味があります。開発するとき、いつもいい設計を考える必要ですから、もっと勉強したいです。
  • <エンジニアになった理由>ソフトウェア、ウェブサイトを作れたら、とても嬉しく思ったからです。
  • <みんマの好きなところ>フレックスタイム

f:id:curama-tech:20180706112812j:plain

TuyenくんもDuyくんと同じベトナム出身です。日本語でのコミニュケーションがうまくいかない時は大体笑ってごまかします。その笑顔は人を和ませる力を持っているので、かなりキャラ得です。Tuyenくんはできる素振りは一切見せずに実はアプリの開発ができたり数学が得意だったりと、まだ明かされてない特技を秘めているような気がします。

さいごに

いかがでしたでしょうか?
もっとエンジニアの話を聞いてみたいと思った方はぜひお気軽にご連絡ください!(コーポレートサイト https://www.minma.jp/

Implementing a simple pipeline with Typescript

Implementing a simple pipeline with Typescript

Hi, this is Sushant from the Minma, Inc. tech-team and today I would like to demonstrate to you the power of Typescript. In my last post, I introduced you to Apache Airflow and demonstrated the implementation of a pipeline to create a pizza using Airflow. Today, we will do the same, but with Typescript. The goal is to build a strongly typed pipeline utilizing Typescript's type system and create a variety of pizzas with it.

As you can probably tell, I am somewhat obsessed to pipelines. To me, pipelines are a great way to write code for some business process. Add static typing to pipelines and you get a pipeline that is robust, modular and flexible and readable. Let's say I work for an e-commerce site (which I actually do ;)) and we just got reports that our users never get any welcome email upon registration. Not knowing if it's a bug in the system or if we ever send emails in the first place, my product manager asks me if we are sending welcome emails when user registration is complete. Being a relatively new programmer in the company, I have no other way to answer that than go through the source code for and find out what happens during user registration. A lot of time this would involve going through a lot of code, jumping around from function to function trying to get to code that relates to the end of the registration process. If the same user registration process were implemented as a pipeline, I would be able to get a top level view of what goes on when a user registers at our site just by looking at the pipeline's steps. Chances are, there could actually be a step in the pipeline that reads "sendWelcomeEmail"! Even if that's not the case, I could just look at the pipeline and choose a step in the pipeline that I think could have a connection to emails and see what's going on.

Enough talk! Let's write ourselves a pipeline with Typescript.

Here's what we are gonna do: - a. Implement a function that lets us create pipeline for any workflow. - b. Use the function to create a pipeline for creating pizzas based on a user's order. - c. Use the pipeline created in step b. to create various kinds of pizzas.

The source code for the pipeline is available in github

a. Implement a function that lets us create pipeline for any workflow.

We want our module to take in functions that represent each step in our pipeline and return a single composed function that performs those steps in order.

If f(x), g(x), h(x) and i(x) represent each step in our pipeline, the definition of our workflow would be

            j(x) = i ( h ( g ( f ( x ) ) ) ) 

So, whenever we want to run our workflow, we don't directly call f(x), g(x), h(x), and i(x) in order every single time. All we need to do is call j(x) and the pipeline runs as expected.

Here is the definition in typescript:

export type Pipeable<Args, ReturnType> = (args: Args) => ReturnType;

export function createPipeline<X, A>(f: Pipeable<X, A>): Pipeable<X, A>;
export function createPipeline<X, A, B>(f: Pipeable<X, A>, g: Pipeable<A, B>): Pipeable<X, B>;
export function createPipeline<X, A, B, C>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>): Pipeable<X, C>;
export function createPipeline<X, A, B, C, D>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>, i: Pipeable<C, D>): Pipeable<X, D>;
export function createPipeline<X, A, B, C, D, E>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>, i: Pipeable<C, D>, j: Pipeable<D, E>): Pipeable<X, E>;
export function createPipeline<X, A, B, C, D, E, F>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>, i: Pipeable<C, D>, j: Pipeable<D, E>, k: Pipeable<E, F>): Pipeable<X, F>;
export function createPipeline<X, A, B, C, D, E, F>( 
    f: Pipeable<X, A>, 
    g?: Pipeable<A, B>, 
    h?: Pipeable<B, C>, 
    i?: Pipeable<C, D>, 
    j?: Pipeable<D, E>, 
    k?: Pipeable<E, F>): 
    Pipeable<X, A> | 
    Pipeable<X, B> |
    Pipeable<X, C> | 
    Pipeable<X, D> | 
    Pipeable<X, E> | 
    Pipeable<X, F> {
        if (!!k) {
            return (args) => k(j!(i!(h!(g!(f(args))))));
        } else if (!!j) {
            return (args) => j(i!(h!(g!(f(args)))));
        } else if (!!i) {
            return (args) => i(h!(g!(f(args))));
        } else if (!!h) {
           return (args) => h(g!(f(args)));
        } else if (!!g) {
            return (args) => g(f(args));
        } else {
            return (args) => f(args);
        }
}

Let's break this down one by one.

export type Pipeable<Args, ReturnType> = (args: Args) => ReturnType;

Pipable<Args, ReturnType> is a generic type for any function that takes in arguments of type Args and returns something of type ReturnType. We will be using this to generate your compose function.

Our main function createPipeline() has six overloads, one for each number of arguments passed to it. This implies that our create pipeline function can handle workflows having no more than 6 steps.
Let's look at one of these overloads:

export function createPipeline<X, A, B, C>(f: Pipeable<X, A>, g: Pipeable<A, B>, h: Pipeable<B, C>): Pipeable<X, C>;

If we were to visualize what createPipeline does, it would look something like this:

It takes in 3 functions f, g, and h and returns a new composed function that does the same thing as the three functions combined. Notice that the input of the composed function is the same as the input of the first step of the pipeline and the output is the same as the output of the last step of the pipeline. What this means is, instead of running f, g, and h in order everytime, we can just run this composed function every time we want to execute our pipeline and be assured that the result will be the same as running f, g and h combined.

if (!!k) {
    return (args) => k(j!(i!(h!(g!(f(args))))));
} else if (!!j) {
    return (args) => j(i!(h!(g!(f(args)))));
} else if (!!i) {
    return (args) => i(h!(g!(f(args))));
} else if (!!h) {
   return (args) => h(g!(f(args)));
} else if (!!g) {
    return (args) => g(f(args));
} else {
    return (args) => f(args);
}

↑This is not code that I am proud of, but in order to ensure that the types are inferred correctly, I had to write it this way.

What I really wanted to do was to implement a reducer like so

 const reducer: <M, N, O>(f: Pipeable<M, N>, g: Pipeable<N, O>) => Pipeable<M, O> = (f, g) => (x) => g(f(x));

and use the reducer function to reduce an array of functions like

[f, g, h, i, j, k].reduce(reducer)

, but sadly Typescript wasn't so forigiving. If anybody has a more elegant solution, please enlighten me!

b. Use the function to create a pipeline for creating pizzas based on a user's order.

This is where we build the building blocks of our pipeline. Let's first define what an order looks like:

export namespace OrderOptions {
    export enum Crust {
        HandTossed = "HandTossed",
        Pan = "Pan",
        ThinCrust = "ThinCrust"
    }

    export enum Sauce {
        Tomato = "Tomato"
    }

    export enum Topping {
        Ham = "Ham",
        Cheese = "Cheese",
        Pineapple = "Pineapple",
        Mushroom = "Mushroom",
        Pepperoni = "Pepperoni",
        ItalianSausage = "Italian Sausage",
        Olives = "Olives",
        Jalapeno = "Jalapeno",
        GreenPepper = "Green Pepper",
        Anchovies = "Anchovies",
        Onion = "Onion"
    }
}

export interface Order {
    crust: OrderOptions.Crust;
    sauce: OrderOptions.Sauce;
    toppings: OrderOptions.Topping[];
}

Customers can select from 3 types of crusts, sadly just one type of sauce and many different kinds of toppings. Our order object, as is obvious from the model, will have a value for crust, sauce and an array of toppings;

Our pipeline for creating a pizza will have the following 4 steps:

  1. Prepare crust.
  2. Apply sauce.
  3. Add toppings.
  4. Bake in oven.

Note that each step is dependent on the previous step. i.e. We cannot apply sauce if you have no crust. We don't add toppings if the sauce hasn't been applied yet, and so on. Therefore, we define state for our pizza as follows:

type CrustPrepared = { crust: OrderOptions.Crust };
type SauceApplied = CrustPrepared & { sauce: OrderOptions.Sauce };
type ToppingsAdded = SauceApplied & { toppings: OrderOptions.Topping[] }
type FreshlyBakedPizza = ToppingsAdded & { baked: true };

type PizzaState = CrustPrepared | SauceApplied | ToppingsAdded | FreshlyBakedPizza;

As we can see, if our pizza is in the SauceAppliedstate, it will have properties from the CrustPrepared state and have one more property for the type of sauce applied. Note that if our pizza is in the ToppingsAddedstate, it will have properties from both the CrustPrepared state the SauceAdded state in addition to the property for the types of toppings used.

To make life easier, we define following type to represent the output at every step in the pipeline:

interface Output<CurrentState extends PizzaState> {
    readonly order: Order,
    pizzaState: CurrentState,
    events: string[];
}

The order property represents the order made by the customer. We don't want the order to suddenly change when the pizza is half done, so we set it as readonly. The pizzaState property represents the current state in the pipeline. We also have an events property for logging purposes so that we can see that all the steps in the pipeline were properly executed and in order.

OK, let's start writing code for each step of the pipeline:

1. Prepare crust.

Our prepareCrust step looks like this:

const prepareCrust: Pipeable<Order, Output<CrustPrepared>> = (order) => {
    return { 
        order: order,
        pizzaState: { crust: order.crust },
        events: [`Preparing ${order.crust} pizza crust......DONE`]
    }
}

Remember that each function passed to our createPipeline function has type Pipeable. Thus, we define our prepareCrust function as Pipeable. We are not doing anything crazy. All we are doing is take the order, and set the pizzaState with crust set to the crust ordered by the user. We also add a message to log that this step completed successfully.

2. Apply sauce.

Similarly, our applySauce step looks like this:

const applySauce: Pipeable<Output<CrustPrepared>, Output<SauceApplied>> = (state) => {
    return { 
        ...state,
        pizzaState: { ...state.pizzaState, sauce: state.order.sauce },
        events: [...state.events, `Applying ${state.order.sauce} sauce......DONE`]
    }
}

Things to note:

  • This function takes the output of the prepareCrust step as an argument.
  • We are using the spread operator ...state to first create a copy of the current state, and then update the state in the copy with the new state.

3. Add toppings.

Our addToppings step looks like this:

const applyToppings: Pipeable<Output<SauceApplied>, Output<ToppingsAdded>> = (state) => {
    return { 
        ...state,
        pizzaState: { ...state.pizzaState, toppings: state.order.toppings },
        events: [...state.events, `Adding ${state.order.toppings.join(", ")} toppings......DONE`]
    }
}

Nothing special here, similar to the applySauce.

4. Bake in oven.

Finally, our bakeInOven step looks something like this:

const bakeInOven: Pipeable<Output<ToppingsAdded>, Output<FreshlyBakedPizza>> = (state) => {
    return { 
        ...state,
        pizzaState: { ...state.pizzaState, baked: true },
        events: [...state.events, "Baking in oven......DONE", "Freshly baked pizza ready!!!!"]
    }
}

Now that we have functions for each of our steps, let's create our pipeline:

export const createPizza = createPipeline(
    prepareCrust,
    applySauce,
    applyToppings,
    bakeInOven
);

We finally get to use our createPipeline function to create our pizza pipeline. Note that if we tried to change the order of the steps here, Typescript would not let us! This is where all the hard work of writing the overloads for createPipeline pays off. This is the part that also shows the readability of pipelines. I could just look at this piece of code and have an idea about what steps are involved in creating a pizza. Of course, good naming goes hand in hand for that to work.

Notice that we are not exporting the individual steps. We are just exporting the composed createPizza function. So, if somebody wants to create a pizza, they have no choice but to adhere to our createPizza pipeline. We can extend this concept and do things differently. Instead of having a generic createPizza for all types of pizzas, we could have a pipeline for each type of pizza we offer such as createHawaiianPizza or createMeatLoversPizza and so on. The possibilities are endless!

Now that we have our pipeline ready, let's order some pizzas:

const handTossedHawaiianPizza = createPizza({
    crust: OrderOptions.Crust.HandTossed,
    sauce: OrderOptions.Sauce.Tomato,
    toppings: [OrderOptions.Topping.Ham, OrderOptions.Topping.Pineapple, OrderOptions.Topping.Cheese]
})

const thinCrustHawaiianPizza = createPizza({
    crust: OrderOptions.Crust.ThinCrust,
    sauce: OrderOptions.Sauce.Tomato,
    toppings: [OrderOptions.Topping.Ham, OrderOptions.Topping.Pineapple, OrderOptions.Topping.Cheese]
})

const meatLoversPizza = createPizza({
    crust: OrderOptions.Crust.Pan,
    sauce: OrderOptions.Sauce.Tomato,
    toppings: [OrderOptions.Topping.ItalianSausage, OrderOptions.Topping.Ham, OrderOptions.Topping.Pepperoni ,OrderOptions.Topping.Cheese ]
})


console.log("######### Hand Tossed Hawaiian pizza ##########")
console.log(handTossedHawaiianPizza)
console.log("###############################################\n")

console.log("######### Thin CrustHawaiian pizza ############")
console.log(thinCrustHawaiianPizza)
console.log("###############################################\n")

console.log("############# Meat Lovers pizza ###########")
console.log(meatLoversPizza)
console.log("###############################################\n")

All we are doing is passing an Order object to our createPizza pipeline and logging the results to the console.

To see if it works, compile with tsc and run.

If using the source code from github (assuming that Typescript is installed), you can test it like so:

# Inside the src directory
$ tsc && node bin/orderPizza.js

Output:

######### Hand Tossed Hawaiian pizza ##########
{ order:
   { crust: 'HandTossed',
     sauce: 'Tomato',
     toppings: [ 'Ham', 'Pineapple', 'Cheese' ] },
  pizzaState:
   { crust: 'HandTossed',
     sauce: 'Tomato',
     toppings: [ 'Ham', 'Pineapple', 'Cheese' ],
     baked: true },
  events:
   [ 'Preparing HandTossed pizza crust......DONE',
     'Applying Tomato sauce......DONE',
     'Adding Ham, Pineapple, Cheese toppings......DONE',
     'Baking in oven......DONE',
     'Freshly baked pizza ready!!!!' ] }
###############################################

######### Thin CrustHawaiian pizza ############
{ order:
   { crust: 'ThinCrust',
     sauce: 'Tomato',
     toppings: [ 'Ham', 'Pineapple', 'Cheese' ] },
  pizzaState:
   { crust: 'ThinCrust',
     sauce: 'Tomato',
     toppings: [ 'Ham', 'Pineapple', 'Cheese' ],
     baked: true },
  events:
   [ 'Preparing ThinCrust pizza crust......DONE',
     'Applying Tomato sauce......DONE',
     'Adding Ham, Pineapple, Cheese toppings......DONE',
     'Baking in oven......DONE',
     'Freshly baked pizza ready!!!!' ] }
###############################################

############# Meat Lovers pizza ###########
{ order:
   { crust: 'Pan',
     sauce: 'Tomato',
     toppings: [ 'Italian Sausage', 'Ham', 'Pepperoni', 'Cheese' ] },
  pizzaState:
   { crust: 'Pan',
     sauce: 'Tomato',
     toppings: [ 'Italian Sausage', 'Ham', 'Pepperoni', 'Cheese' ],
     baked: true },
  events:
   [ 'Preparing Pan pizza crust......DONE',
     'Applying Tomato sauce......DONE',
     'Adding Italian Sausage, Ham, Pepperoni, Cheese toppings......DONE',
     'Baking in oven......DONE',
     'Freshly baked pizza ready!!!!' ] }
###############################################

And that's it for this post! I hope I could demonstrate the usefulness of pipelines, especially with Typescript. I'm sure there are better ways to implement the same (and I would be all ears for better ways). Hopefully in a future post, I will be able to demonstrate a better way to implement the same pipeline, perhaps with some error handling and much cleaner code!

PS: We are hiring! For those interested, feel free to apply here . At least conversational Japanese proficiency is required.

Pythonでメモリ使用量を改善してみる

こんにちはエンジニアののりすけです。

みんなのマーケットではPythonを使用したサービスを動かしています。以前より使用メモリが異常に大きいサービスがあるため、SREチームからなんとかしてほしいとの依頼が来ています。

今回はサンプルコードを使ってPythonのメモリプロファイルを行いながら、省メモリなアプリケーションをどのように実装するか検証したいと思います。

利用するツール

  • memory_profiler
  • matplotlib

上記のライブラリをpip installでインストールしておきます。

memory_profilerの基本的な使い方

まず使い方を確認します。以下のように確認したい処理にデコレータ@profileを記述します。

from memory_profiler import profile


def large_integer_list():
    return [i for i in range(0,10000000)]


@profile
def main():
    result = sum(large_integer_list())
    print(result)

if __name__ == '__main__':
    main()

上記のコードを実行すると以下のような結果が出力されます。

$ python sample1.py
49999995000000
Filename: sample1.py

Line #    Mem usage    Increment   Line Contents
================================================
     8     12.4 MiB     12.4 MiB   @profile
     9                             def main():
    10     17.0 MiB      4.6 MiB       result = sum(large_integer_list())
    11     17.0 MiB      0.0 MiB       print(result)

左にあるMem usageが該当の行が評価された時点でのメモリ使用量、Incrementが評価された事による使用量の増加を表しています。今回の例の場合、listに入っている数字の合計を算出した時点で4.6MiB増えたことになっています。

この結果を少し考えたいと思います。

もともとの想定では大きなリストを作成し、メモリを大きく使うことを想定していましたが、このプロファイル結果では4.6MiBしか増えていないように見えてしまいます。しかし、よく考えるとこの部分はsum([int]) を評価した結果が+4.6MiBであり、sum関数を処理する上で不要になった情報は削除されていっているように考えられます。

今度は時系列に推移を見たいため、memory_profilermatplotlibを使って実行時のメモリ使用量を可視化します。

$ mprof run sample1.py
$ mprof plot

このようにmprofコマンドを利用するとグラフ化することができます。グラフから最大400MiB近くまでメモリを使用したことがわかりました。

f:id:curama-tech:20180621220646p:plain

次はこの400MiB近くまで使用してしまうこのコードを改善します。

メモリ使用量の改善

改善案としてlarge_integer_list()関数をリスト型ではなく、ジェネレータを返すように変更します。

from memory_profiler import profile

def large_integer_generator():
    # ここをGenerator式に変更
    return (i for i in range(0,10000000))


@profile
def main():
    result = sum(large_integer_generator())
    print(result)

if __name__ == '__main__':
    main()

実行結果

mprof run sample1.py
mprof: Sampling memory every 0.1s
running as a Python program...
49999995000000
Filename: sample1.py

Line #    Mem usage    Increment   Line Contents
================================================
     8     12.8 MiB     12.8 MiB   @profile
     9                             def main():
    10     12.8 MiB      0.0 MiB       result = sum(large_integer_generator())
    11     12.8 MiB      0.0 MiB       print(result)

f:id:curama-tech:20180621220837p:plain

今度はグラフ上もメモリ使用量が劇的に減っていることがわかります。ジェネレータを使うことで一度に大量のデータを確保することなく一つずつ処理しているため省メモリで処理することができます。

しかし、グラフのx軸を確認すると20秒を超える実行時間になっています。先程のリストを利用した場合は10秒を切る実行時間だったのに対して2倍以上遅い。。。

実行速度の改善

先程のコードではメモリの使用量は改善できましたが、今度は実行速度が問題になってしまいました。原因として考えられるのはジェネレータではyieldするたびに値の評価が行われます。評価回数があまりに多くなってしまったため速度が低下したことが考えられます。

上記の問題点を改善したコードが以下になります。

from memory_profiler import profile
# Iteratorに対する便利な関数がまとまったモジュール
from itertools import (islice, chain)

# iteratorをchunkサイズに分割する関数
def chunks(iterable, chunk_size):
    iterator = iter(iterable)
    chunk = tuple(islice(iterator, chunk_size))
    while chunk:
        yield list(chunk)
        chunk = tuple(islice(iterator, chunk_size))

def large_integer_generator():
    chunk_size = 10000
    # chunk毎にsumを行う
    for chunk in chain(chunks(range(0,10000000), chunk_size)):
        yield sum(chunk)

@profile
def main():
    result = sum(large_integer_generator())
    print(result)

if __name__ == '__main__':
    main()

実行結果

mprof run sample1.py
mprof: Sampling memory every 0.1s
running as a Python program...
49999995000000
Filename: sample1.py

Line #    Mem usage    Increment   Line Contents
================================================
    19     12.6 MiB     12.6 MiB   @profile
    20                             def main():
    21     15.0 MiB      2.4 MiB       result = sum(large_integer_generator())
    22     15.0 MiB      0.0 MiB       print(result)

f:id:curama-tech:20180621220855p:plain

前回のコードよりchunk分のメモリ使用量は増えていますが、速度が劇的に改善されました。

最後に

今回の検証ではうまくメモリ使用量/実行速度の改善を行うことができました。実際のサービスで同様に改善できる部分があるかは、これから確認しなければいけませんが、今後の実装においても良い検証ができたかと思います。

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/ ) 次回はエンジニアのカーキくんの予定です。

Varnish 503 Error

f:id:curama-tech:20180614114810p:plain

こんにちは、SREチームの千代田です。

サイトを構築するにあたって様々なミドルウェアを扱うことがあると思いますが、
今回はキャッシュサーバであるVarnishについてのお話です。

遭遇

私が入社して一ヶ月ほどたったとき、運用中のシステムで発生している既存のエラーについて学ぶ機会がありました。
話を聞きつつどのようなエラーが存在するのかを確認すると、以下のVarnishエラーを複数見つけました。

<!DOCTYPE html>
<html>
  <head>
    <title>503 Backend fetch failed</title>
  </head>
  <body>
    <h1>Error 503 Backend fetch failed</h1>
    <p>Backend fetch failed</p>
    <h3>Guru Meditation:</h3>
    <p>XID: 532316890</p>
    <hr>
    <p>Varnish cache server</p>
  </body>
</html>

このエラーの原因について既存のメンバーに確認すると、
複数ある特定のURLで起こる、POST/PUTのリクエストのbodyが短いと起きるなど諸説あり、発生原因はまだ特定できていないとのことでした。

調査

私はこの時Varnishに対して全く無知でした。
そのため、公式のドキュメントを頼りにトレースログを行うことにしました。

Varnishは高速で動作させるために、ログの出力をVSLと呼ばれる共有メモリセグメントに行なっています。
このログデータは一つのリクエストに対して大量の行数が出力されるため、日常的なログの出力を行っていませんでした。
膨大なログからデータを見つけるのはとても大変な作業のため、
このログをファイルに出力させつつ既存のログ側でエラーを検知した際に、
該当のVarnishログを見るといった方針で行なうことにしました。

そして、Varnishのログを取得することに成功しました。
VarnishのStateMachineについては、 公式を見ると分かりやすいと思います。

*   << BeReq    >> 576325623 
-   Begin          bereq 576325622 pass
-   Timestamp      Start: 1504590151.903285 0.000000 0.000000
-   BereqMethod    PUT
-   BereqURL       /v1/microservice/
-   BereqProtocol  HTTP/1.0
-   BereqHeader    host: xxx.yyy
-   BereqHeader    Content-Length: 4623
-   BereqHeader    accept: application/json
-   BereqHeader    content-type: application/json
-   BereqHeader    X-Varnish: 576325623
-   VCL_call       BACKEND_FETCH
-   VCL_return     fetch
-   BackendOpen    46 reload_YYYY-MM-ddWHH:mm:ss.backend_name x.x.x.x 80 x.x.x.x 56894
-   BackendStart   x.x.x.x 80
-   Timestamp      Bereq: 1504590151.903331 0.000046 0.000046
-   FetchError     http first read error: EOF
-   BackendClose   46 reload_YYYY-MM-ddWHH:mm:ss.backend_name
-   Timestamp      Beresp: 1504590151.907966 0.004681 0.004635
-   Timestamp      Error: 1504590151.907977 0.004692 0.000011
-   BerespProtocol HTTP/1.1
-   BerespStatus   503
-   BerespReason   Service Unavailable
-   BerespReason   Backend fetch failed
-   BerespHeader   Server: Varnish
-   VCL_call       BACKEND_ERROR
-   VCL_return     retry
-   Timestamp      Retry: 1504590151.907986 0.004701 0.000009
-   Link           bereq 577078686 retry

http first read error: EOFといったエラーが出ていることが確認できます。
このエラーについて検索するとstackoverflowやvarnishのmailing listなど、様々な媒体の情報が見つかりました。
しかしながら、どれを読んでも今回のエラーに対して思い当たる節がありませんでした。

また、Varnishからのリクエストを受けるDjangoのエラーログには以下のログが出力されていました。

django.http.request.UnreadablePostError: error during read(4623) on wsgi.input

こちらのエラーを確認すると、
先ほどVarnishから送ったリクエストに対してのContent-Length分のbody readに失敗したエラーに見えます。
この時点で私の中では、
バージョンの古いVarnishを利用していることによってバグでリクエストが破損しているのではないかといった結論に至りました。
そのため、後日Varnishのバージョンアップを行なうことにしました。

バージョンアップ後の調査

バージョンを上げたにも関わらず相変わらず503エラーが続いていました。
しかし残念ながら、別の仕事もある都合上いつまでも調査を行うわけにもいかず一度保留することにしました。

さらに数ヶ月後、手が空いた際に再度この問題に着手することができました。
前回の調査結果を念頭に置きつつ、今回は別のリクエストをパケットレベルで追うことにしました。
今回のリクエストは、以下の流れです。

BFF -> Varnish -> Microservice

まず始めにBFF -> Varnish間のパケットをtcpdumpで確認します。
余分なヘッダ/データを削っているため、lengthは若干異なります。

HH:mm:ss IP ip-xxx-xxx-xxx-xxx.40596 > ip-yyy-yyy-yyy-yyy: Flags [P.], seq 1:328, ack 1, win 211, options [nop,nop,TS val 1858274581 ecr 947885993], length 327
POST /v1/microservice2/ HTTP/1.0
Connection: close
Content-Length: 100
accept: application/json
content-type: application/json

{"xxxxxxxx":"XXX","xxxxxxxxxxxx":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}

この間は問題なく、データがきているように見えます。

次にVarnish -> Microservice間のパケットです。

HH:mm:ss IP ip-yyy-yyy-yyy-yyy.40324 > ip-zzz-zzz-zzz-zzz: Flags [P.], seq 1:260, ack 1, win 211, options [nop,nop,TS val 947885994 ecr 947921750], length 259
POST /v1/microservice2/ HTTP/1.0
Content-Length: 100
accept: application/json
content-type: application/json
X-Varnish: 888442338

リクエストは送られてきましたが、Content-Length分のbodyがありません。
しかしながら、Varnishのリクエストが503だった場合にretry処理を呼ぶようにしてあります。
今度はそちらのリクエストを確認しました。

HH:mm:ss IP ip-yyy-yyy-yyy-yyy.46642 > ip-zzz-zzz-zzz-zzz: Flags [P.], seq 247:606, ack 38176, win 843, options [nop,nop,TS val 947885994 ecr 947945014], length 359
POST /v1/microservice2/ HTTP/1.0
Content-Length: 100
accept: application/json
content-type: application/json
X-Varnish: 888672061

{"xxxxxxxx":"XXX","xxxxxxxxxxxx":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}

再送されたリクエストにはbodyがありました。
lengthも259 -> 359へとContent-Length分増えていることが分かります。
この時点で、VarnishによってPOSTのbodyが存在したりしなかったりすることが分かりました。
しかし残念ながら、なぜbodyが消えてしまうのかまでは今回の調査では分かりませんでした。

おわりに

Backendとの相性が悪いのかを確認するために、uWSGIを辞めてGunicornに変えるなど試しましたが、
相変わらず503エラーが起こったため、少なくともバックエンド起因ではないと考えています。
また、次のブログを参照し、
varnishでbackendのapacheにPOSTしているときにたまに(1/1000ぐらい)503 Service Unavailable になる現象はpassをpipeに変えたら出なくなった
実際に、Varnishのpassをpipeに変えれば、POST/PUTによる503が出なくなることの確認が取れました。
そのため、根本的な解決とはいかず不完全燃焼ですが、今回の調査をもって一旦解決とすることにしました。

最後までお読みいただき、ありがとうございました。
今回の問題に対して他のアプローチを思いつく方や、
システムへの調査/改善/開発など多岐に渡る業務を行いたいと思っている方がいらっしゃれば、
ぜひ一度オフィスでお会いしましょう

次回は、エンジニアののりすけさんの予定です。

あの日呼び出したモジュールの名前を僕達はまだ知らない

はじめに

みんなのマーケットで iOS / Android アプリエンジニアとして働いているYangです。

非常に分(意)か(味)り(不)や(明)す(!)く(!)タイトルに書いた通り、今回はデカップリングを話題として、Androidのモジュール化を紹介したいと思います。

モジュール化とは

イメージとしては:

f:id:curama-tech:20180608115351j:plain

(モザイク除去はこちらへ画像の出典)

違う!

複雑で巨大なシステムやプロセスを設計・構成・管理するとき、全体を機能的なまとまりのある“モジュール”に要素分割すること。設計・製造時の擦り合わせ作業をできるだけ少なくするために構成要素(部品)の規格化・標準化を進め、その相互依存性を小さくすることをいう。出典

「wwwの生みの親」「Webの父」などの異名を持つ、ティム・バーナーズ=リーさんの本「Principles of Design」に以下の名言があります。

Principles such as simplicity and modularity are the stuff of software engineering; decentralization and tolerance are the life and breath of Internet.

会社の事業拡大と共に、アプリの機能もどんどん増えて、複雑になっています。

巨大とまではいかなくてもビルドに時間はかかるし、ファイルツリーがだんだん長くなって、開発もしづらく、依存が多すぎて単体テストが書きづらいです。また、部分的な不具合や変更が発生したときに、その影響が全体へと波及してしまいます。

解決しないといけないこと

1. コードのデカップリング(分離原則)

 どうやって一つの巨大のプロジェクトを複数のモジュールに分けられますか。もしモジュール間で、再度お互いに直接アクセスするのであれば、デカップリングとは全く言えません。どうすれば直接の参照を避けられるでしょうか。

2. 各モジュールの独立実行

 一回のデバッグにもし一つか二つぐらいのモジュールだけ参加させたら、ビルドの時間は大幅に短縮できますから、開発の段階でどうやって最低限必要なモジュールだけをコンパイルさせますか。各モジュールが疎結合であれば,どうやって独立に実行やデバッグをさせますか。

3. 各モジュール間のデータ通信

 各モジュールは外部へのサービスを提供することができますが、メインモジュール(Host)や他のモジュールへどうやってデータを送受信すればいいでしょうか。

 実際画面遷移の本質も特殊なデータ送信ですが、Android上の画面遷移は Intent を使わないといけないので、解決の方法は少し違います。

コードのデカップリングの解決

巨大なコードを分割しやすくするために、Android Studio IDEに Multiple Module という機能をサポートしていて、これを使って最初に簡単な分離はできます(つまり参照が複雑なところは自分で整理しないといけないです)。

ここで明確したい概念があって、モジュールは二種類に分けることができます。

一つは Base Library で、これらは他のモジュールから直接参照できます。例えば、ネットワークモジュールや画像処理モジュールは二つの Base Library として考えられると思います。

もう一つは Component と似ているような感じで、完全な機能を持っています。くらしのマーケットのアプリを例にすると、予約モジュールやチャットモジュールは二つの Component と呼ばれたらいいです。

Base Library は単純に汎用的なサービスを提供するため、 モジュール化 に言った モジュール は基本的に二つ目です。

これで、コードの分離は図の通りにできます。

f:id:curama-tech:20180608115345p:plain

矢印を書いているところだけ依存関係が存在して、つまり Reservation ModuleCalendar Module はお互いに見えないです。こうすると、コードのデカップリングを実現できます。

各モジュールの独立実行の解決

ビルドツールに対して、あるモジュールがプロジェクトの入り口かどうかの判断は該当モジュールの build.gradle を参照しています。

apply plugin: 'com.android.library' の場合は部品で、 apply plugin: 'com.android.application' の場合はアプリと識別されます。

つまり一個 isRunAlone の変数を用意すれば、切り替えは可能です。

これ以外、アプリと識別された場合、 AndroidManifest.xml ファイルに最初ページとしての Activity も指定する必要があります。私の解決方法では二つのManifestファイルを用意して、リリースの際にdebug用のファイルを除外します。

最後、アプリとして実行するために必要な applicationId も指定して、独立実行の準備はできました。

if (isRunAlone.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
// ......
    defaultConfig {
        if (isRunAlone.toBoolean()) {
            applicationId "jp.curama.shop.module1"
        }
    // ......
    }
// ......
    sourceSets {
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/release/AndroidManifest.xml'
                java {
                    exclude 'debug/**'
                }
            }
        }
        // ......
    }
// ......

各モジュール間のデータ通信の解決

前書いた通り、ここは二つのシーンがあって、①画面遷移と②メソッド呼び出しです。色々調べて、一番良い解決案はAlibabaが提供している Open Source Library ARouter です。一発で二つのシーンを解決できます。

Android の標準画面遷移は下の通りです。ここでの問題は、 ActivityAActivityB をインポートしないと遷移できません。

// filePath: module1/ActivityA.kotlin
// module2への依存が発生してしまいました
import module2.ActivityB

class ActivityA: BaseActivity() {
    fun navigateToB(id: String) {
        val intent = Intent(this, ActivityB::class.java).apply { putExtra("id", id) }
        startActivity(intent)
    }
}

// filePath: module2/ActivityB.kotlin
class ActivityB: BaseActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState);
        val id = intent?.getStringExtra("id")
        // ......
    }
}

ARouterを使う場合、 ActivityA は遷移先の URL がわかったら遷移できます。私達のシステムでは管理上の考えによって、 RouterPath という URL の集約ファイルを作って、 Base Business Layer に置いて、 URL のハードコードを避けました。またどの画面がどのURLを使っていて、必要なパラメーターが何であるかは誰でもすぐに一目瞭然で、把握できます。もう一つ便利なところは ARouter 経由で渡したパラメーターがインジェクターによって自動的に遷移先へアサインされるところです。

// filePath: module1/ActivityA.kotlin
// import ActivityB は不要です
class ActivityA: BaseActivity() {
    fun navigateToB(id: String) =
        ARouter.getInstance().build("module2/b").withString("id", id).navigation()
}

// filePath: module2/ActivityB.kotlin
@Route(path = "module2/b")
class ActivityB: BaseActivity() {
    @Autowired
    lateinit var id: String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState);
        ARouter.getInstance().inject(this);
        // ここから this.id はもう使えます
        // ......
    }
}

シーン②も似たやり方で、 URL でアノテーションをして、使用者はインタフェースに依存することで、ARouterから実現を取得して、使ったらうまく動けます。

これでモジュール化の大きい問題を全部解決できました。

最後に

我々みんなのマーケットテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています!どんな環境で開発しているかはこちらの記事にまとまっています。興味がある方はぜひ気軽に連絡ください (コーポレートサイト https://www.minma.jp/

次回は、SREチームの千代田さんの予定です。