読者です 読者をやめる 読者になる 読者になる

TenForward

技術ブログ。はてなダイアリーから移転しました

Linux 3.3 の新機能 Per-cgroup TCP buffer limits (2)

Linux

(2014-03-06 追記) この部分のコード,sysctl パラメータ経由で TCP buffer に対して制限をかけるというコードはうまく動かないようで Linux 3.13 で削除されています.これがなんか値が合わないなあ,ってなってた原因ですかね? (新しいコード見てないのでわかりません :p) > tcp_memcontrol: Remove setting cgroup settings via sysctl

Linux 3.3 の新機能 Per-cgroup TCP buffer limits - TenForwardの日記 の続編です.カーネルのコードを読んだりしていますが,私はその辺りの知識は殆どありませんので,間違いがある場合は指摘していただけるとありがたいです.

Per-cgroup TCP buffer limits に関するドキュメント

この制限を設定するには cgroup filesystem の memory.kmem.tcp.limit_in_bytes というファイルに値をセットするわけですが,この機能に関するドキュメントはカーネルソースの Documentation/cgroup/memory.txt 内の

  :
75	 memory.kmem.tcp.limit_in_bytes  # set/show hard limit for tcp buf memory
76	 memory.kmem.tcp.usage_in_bytes  # show current tcp buf memory allocation
  :
261	2.7 Kernel Memory Extension (CONFIG_CGROUP_MEM_RES_CTLR_KMEM)
262	
263	With the Kernel memory extension, the Memory Controller is able to limit
264	the amount of kernel memory used by the system. Kernel memory is fundamentally
265	different than user memory, since it can't be swapped out, which makes it
266	possible to DoS the system by consuming too much of this precious resource.
267	
268	Kernel memory limits are not imposed for the root cgroup. Usage for the root
269	cgroup may or may not be accounted.
270	
271	Currently no soft limit is implemented for kernel memory. It is future work
272	to trigger slab reclaim when those limits are reached.
273	
274	2.7.1 Current Kernel Memory resources accounted
275	
276	* sockets memory pressure: some sockets protocols have memory pressure
277	thresholds. The Memory Controller allows them to be controlled individually
278	per cgroup, instead of globally.
279	
280	* tcp memory pressure: sockets memory pressure for the tcp protocol.

これだけですね.

TCP が使うメモリ量のシステム全体の制御

ところでこれ以前にシステム全体の TCP のメモリを制御する sysctl パラメータに net.ipv4.tcp_mem というものがあるのをご存知の方も多いかと思います.

$ sudo sysctl -a | grep net.ipv4.tcp_mem
net.ipv4.tcp_mem = 191406	255208	382812

この三つの値は最初から最小値,pressure値,最大値です.Pressure というのは,この値を超えた所でカーネルTCP が使うメモリ量の圧縮を始めます.そしてこの圧縮モードは消費量が min の値以下になるまで続きます.詳しくは Documentation/networking/ip-sysctl.txt をどうぞ.

312	tcp_mem - vector of 3 INTEGERs: min, pressure, max
313		min: below this number of pages TCP is not bothered about its
314		memory appetite.
315	
316		pressure: when amount of memory allocated by TCP exceeds this number
317		of pages, TCP moderates its memory consumption and enters memory
318		pressure mode, which is exited when memory consumption falls
319		under "min".
320	
321		max: number of pages allowed for queueing by all TCP sockets.
322	
323		Defaults are calculated at boot time from amount of available
324		memory.

では,これと Per-cgroup TCP buffer limits の関係は? (私は最初は全く関係ないと思ってました)

コードを読む

カーネルのコードで memory.kmem.tcp.limit_in_bytes を設定している部分を調べてみました.このあたりのコードを理解するにはまず,Documentation/cgroups/resource_counter.txtを見ておくと cgroup のリソースコントロールの仕組みが分かって良いかもしれません.

この機能の cgroup を通じての値の入出力は net/ipv4/tcp_memcontrol.c で記述されています.ファイルの冒頭の方で

static struct cftype tcp_files[] = {
        {
                .name = "kmem.tcp.limit_in_bytes",
                .write_string = tcp_cgroup_write,
                .read_u64 = tcp_cgroup_read,
                .private = RES_LIMIT,
        },
   : (snip)
};

という風に定義されており,memory.kmem.tcp.limit_in_bytes の設定は tcp_cgroup_write 関数,読み取りは tcp_cgroup_read 関数で行うことになっています.tcp_cgroup_write は短い関数で

static int tcp_cgroup_write(struct cgroup *cont, struct cftype *cft,
                            const char *buffer)
{
        struct mem_cgroup *memcg = mem_cgroup_from_cont(cont);
        unsigned long long val;
        int ret = 0;

        switch (cft->private) {
        case RES_LIMIT:
                /* see memcontrol.c */
                ret = res_counter_memparse_write_strategy(buffer, &val);
                if (ret)
                        break;
                ret = tcp_update_limit(memcg, val);
                break;
        default:
                ret = -EINVAL;
                break;
        }
        return ret;
}

となっています.res_counter_memparse_write_strategy はファイルに書かれた内容の読み取りを行っており,kernel/res_counter.c で定義されています.実質はこんな感じ.

int res_counter_memparse_write_strategy(const char *buf,
                                        unsigned long long *res)
{
    : (snip)
        *res = memparse(buf, &end);
    : (snip)
        *res = PAGE_ALIGN(*res);
        return 0;
}

memparse は,例えば cgroup ファイルシステムのファイルに echo "10M" > memory.kmem.tcp.limit_in_bytes みたいに "K" とか "M" とか付けた場合にバイトへの変換を行っているだけの関数で lib/cmdline.c にあります.

PAGE_ALIGN は,設定値の次のページ境界を取得するマクロで (include/linux/mm.h)

/* to align the pointer to the (next) page boundary */
#define PAGE_ALIGN(addr) ALIGN(addr, PAGE_SIZE)

これで値がページ単位 (x86 の場合 4096) 毎になるんだと思います.

で,この後実際に tcp_update_limit で設定されます.

static int tcp_update_limit(struct mem_cgroup *memcg, u64 val)
{
    : (snip)
        for (i = 0; i < 3; i++)
                tcp->tcp_prot_mem[i] = min_t(long, val >> PAGE_SHIFT,
                                             net->ipv4.sysctl_tcp_mem[i]);
    : (snip)

        return 0;
}

色々処理はありますが,実質は上記がコアの部分の処理で,

  • min_t はマクロで,上記だと long 型で比較を行い,val >> PAGE_SHIFT の値と net->ipv4.sysctl_tcp_mem[i] の値を比較して小さい方を取得する
  • PAGE_SHIFT は x86 の場合 12 (つまり 1<<12 = 4096)
  • tcptcp_prot_mem[3] は,初期化関数の tcp_init_cgroup 内で sysctl の tcp_mem の値を入れているので,sysctl の tcp_mem の 3 つのパラメータと同じ意味と思われる.こんな感じ
            tcp->tcp_prot_mem[0] = net->ipv4.sysctl_tcp_mem[0];
    tcp->tcp_prot_mem[1] = net->ipv4.sysctl_tcp_mem[1];
    tcp->tcp_prot_mem[2] = net->ipv4.sysctl_tcp_mem[2];

つまり tcp_mem の最小値,pressure値,最大値のそれぞれの値を memory.kmem.tcp.limit_in_bytes の値を比較し,小さい方を新しい設定値として設定していることになります.結構乱暴ですが,制限を行うのに新たな仕組みを作ることなく,従来からある仕組みをうまく利用しているコードということになりますね.

結局どういう制限がかかるのか?

Per-cgroup TCP buffer limits の機能を有効にすると,

  • sysctl で設定される net.ipv4.tcp_mem で設定されて適用されるような制限が cgroup で定義したグループ毎にかかるようになる

そしてその制限は,memory.kmem.tcp.limit_in_bytes の値が

  • 4096 (x86の場合) 以下の場合:"4096 4096 4096" という変な(?)設定になる
  • tcp_mem の最小値より大きく pressure より小さな値の場合:"最小値 memory.kmem.tcp.limit_in_bytesの値 memory.kmem.tcp.limit_in_bytesの値" というこれも若干変な(?)設定になる
  • pressure より大きく最大値より小さい場合:"最小値 pressure値 memory.kmem.tcp.limit_in_bytesの値" という設定になる.これは自然ですかね.
  • 最大値より大きい場合:net.ipv4.tcp_mem の設定がそのまま使われる

ということになります.

先に示した文書などでは sysctl との関係は全く触れられておらず,この機能を有効にすると tcp_mem がグループ毎に効くようになる,というのは全くわかりませんね.もう少し機能がちゃんと実装されたら (soft_limit とか),情報も充実するのでしょうか? (^^;)

(ちなみに sysctl で設定した値が cgroup 毎にかかる,というのはこの機能関連のコミットの際のログを見ればそれっぽい事は書いてあります.でもまあ普通はコミットログなんて見ないわなー ^^;)

(Linux 3.3 の新機能 Per-cgroup TCP buffer limits (3) - TenForwardの日記へ続く)