2ヶ月前の内定者バイトを今更振り返る
こんにちは!
8~9月にサイバーエージェントにて内定者アルバイト(エンジニア)してきましたので、
軽く振り返りの意味を込めて記事にしました。終わってから2ヶ月も経ってしまってしまいました…
配属部署について
OPENREC.tvというサイバーエージェントの子会社CyberZで展開しているサービスのサーバサイドエンジニアとして配属されました。
OPENREC.tvはゲーム動画配信プラットフォームで、OPENRECキットといったゲーム配信に特化した面白い機能を提供しています。OPENRECキットは、配信を豪華に演出できるエフェクトツールで、視聴者とのコミュニケーションに連動して動くスタンプを自作して表示したりできます。
今回の目標
今回の内定者バイトで身に付けたかったことは以下でした。
- 保守運用を見越した設計や実装ができるようにする
- 今まで触ってこなかった言語に触れる
学生レベルではどうしても運用のことをあまり考えず、実装することが多いため、この機会で少しでも意識できるようにしたいと思っていました。
言語はあくまで開発するツールなので、開発方法やプロダクトに適した言語を選んで利用できるのが理想です。そのため、選択肢が多いことはエンジニアの武器だと思うので今回今まで触れてこなかった言語に挑戦しようと思いました。(中途半端に使える言語がたくさんあっても仕方ないですが…)
割り当てられたタスク
今回割り当てられたタスクは大まかに以下のようなものです。
CRUD+APIについて
Kotlin(CRUD+API)とTypeScript(API/BFF)を扱いました
(どちらもほぼ初めて触る言語です)。
CRUDやAPIに関してはそんなに説明する必要もないと思うので、
軽く触れるだけにします。
KotlinはWebフレームワークとしてKtor、ORMはExposedを利用して、TypeScriptのフレームワークとしてはExpressを利用しました。
比較的シンプルな言語だったのに加えて、コーディングルールがしっかりしていたため、とても書きやすかったです。仮に自分が今後所属するチームで新しい言語を導入する場合、このような環境を目指すべきだと思いました。
ログ設計について
「ログの目的やフォーマットは大事だ」
という記事はよく目にしますが、どういう手順で何を基準に決めていけばいいか、というのを説明するのは難しいと思います。経験則や勘に頼っているところが多く、正解もないものだと個人的に思っています。
ただ一番大事なのは「いざという時にログで流れを追うことできる」ということです。
今回、私はユーザから問い合わせがあった際に調査/対応しなければいけない場合を想定して取り組みました。シーケンス図とソースコードをみながらユーザが損をするケースを列挙し、対応や補填をすればいいかを考えることで、ログとして必要な情報を検討しました。
学び
学びは正直たくさんありました。
自分のコードの癖の把握
自分がよくやっている書き方とかレビュー反映を一部忘れるとか、普段の自分では気づきにくい、よくないコードや取り組み方をしっかり知ることができました。
(コードレビューが本当にすごかったです、感謝しかないです)
設計書の更新と開発速度のバランスに適応する
開発しつつ設計書を更新するというのは大切なことですが、設計書の更新をせず開発をするというのはよくあることです。
ドキュメントにはない設計(インタフェース部分)や共通認識がたまにあったりして、社員さんと自分の認識に相違が若干生まれたりしたこともありました。
もう少し現場の状況を把握しながら、取り組んでいればうまく回避できたかなと思います。また、設計書で足りない部分をいつの間にか補完していたということもよくなかったと思いました。補完自体は悪くないと思いますが、確認をちゃんと取らなかったことは失敗だったと思います。自分は補完する人間なんだと把握できただけでも、この失敗はあって良かったと思います。
システムの中で何を正しいとするか
これが一番印象に残っているのですが、ログ設計をしていく中で「何を信頼するのか」をしっかり決めないと、うまく流れを追うことはできないと指摘されました。
実際に、「ログは欠損/重複するもの」「DBは不正な値が入ったら終わり」「コードが正しく動作するとは限らない」「ユーザが嘘をついているかもしれない」という感じで全てを疑って総合的に判断するというスタイルだったので、軸が定まっていませんでした。
そこで言われた一言が
「ユーザに届く情報はDBのデータ、
ユーザにとっては届いた結果が全て」
これはよく考えれば当たり前のことではありますが、当時の私は衝撃を受けました。DBのデータに関係なしに、ユーザにはその情報が届き、その結果によってユーザは行動を起こすということです。
DBの変更履歴は別のDBに保存することで変更したことを保証できますが、誰からのアクセスでどんなデータによってその変更が行われたのかまでは把握できません。
そこで、ログを使ってデータを追えるようにするんだと理解しました。ログと全体的な関係が見えてきて大変勉強になりました。
Clean Architecture
想定外の大きい収穫としてはClean Architectureについて理解できたことです。チーム内のWikiにあった簡単な解説がかなり分かり易かったこともあり、実装しながらどの層がどんな役割を担っていて、どこと繋がりどんな流れで実行されていくかがわかりました。
今の自分の立ち位置
自分がどの程度のエンジニアで、実際の現場で何ができるのかというのを、今までは把握する機会と余裕がなかったのですが、今回は研究とかPBLではなく実際の現場における自分のレベルを少しは知ることができました。
まとめ
非常に成長することができ、楽しかった1ヶ月間でした!!学生では普段経験することないことばかり経験することができ、本当に感謝しています。
本当に社員みなさんが優しく良い人たちで安心して取り組むことができました。機能提案して実装している同期もいました。(本当にすごい……!)
自分も次は新機能を提案したいなとか、ぜひ次もサービスに携わりたいなと思いました。
CRIUのメモリ取得について
こんにちは。お久しぶりです。
本日はCRIU(Checkpoint / Restore in Userspace)が行なっているメモリ取得に関する部分を書いていきます。
CRIUのメモリ取得
プロセスの状態にはプロセスが利用しているメモリの状態が含まれます。
メモリには大きく分けて以下の領域が存在します。
テキスト領域:プログラム本体 ELF
データ領域:staticがつけられた変数を保持
ヒープ領域:mallocなどで動的に増したメモリを保持
スタック領域:変数や関数の呼び出し順番などを保持
ライブラリ:libcなど
メモリの状態を取得するわけですが、CRIUは割り当てられたメモリを全て取得しているわけではありません。
CRIUのメモリ取得条件
CRIUはページ単位(4KB, 0x1000)でメモリを取得しています。取得条件の判定はshould_dump_page()で行なっています。
mem.c#L181
for (pfn = 0; pfn < nr_to_scan; pfn++) { unsigned long vaddr; unsigned int ppb_flags = 0; int st; if (!should_dump_page(vma->e, at[pfn])) continue; vaddr = vma->e->start + *off + pfn * PAGE_SIZE; if (vma_entry_can_be_lazy(vma->e) && !is_stack(item, vaddr)) ppb_flags |= PPB_LAZY;
should_dump_page()の関数内部は以下になってます。returnがtrueになるページが取得すべきページです。
mem.c#L102
bool should_dump_page(VmaEntry *vmae, u64 pme) { /* * vDSO area must be always dumped because on restore * we might need to generate a proxy. */ if (vma_entry_is(vmae, VMA_AREA_VDSO)) return true; /* * In turn VVAR area is special and referenced from * vDSO area by IP addressing (at least on x86) thus * never ever dump its content but always use one provided * by the kernel on restore, ie runtime VVAR area must * be remapped into proper place.. */ if (vma_entry_is(vmae, VMA_AREA_VVAR)) return false; /* * Optimisation for private mapping pages, that haven't * yet being COW-ed */ if (vma_entry_is(vmae, VMA_FILE_PRIVATE) && (pme & PME_FILE)) return false; if (vma_entry_is(vmae, VMA_AREA_AIORING)) return true; if ((pme & (PME_PRESENT | PME_SWAP)) && !__page_is_zero(pme)) return true; return false; }
上から順番に見ていきます。
if (vma_entry_is(vmae, VMA_AREA_VDSO)) return true; if (vma_entry_is(vmae, VMA_AREA_VVAR)) return false;
そのページがvDSOの領域なら取得すべきページです。
vDSOとは、システムコールの実行を早くするライブラリです。
通常のシステムコールはカーネルモードへ切り替え(コンテキストスイッチ)が必要ですが、頻繁に行われるとパフォーマンスに影響するので、出来るだけコンテキストスイッチを行わないでシステムコールを実行するようにしてます。
VVARは取得しないページです。
VVARはvDSOの一部で、vDSOで利用する変数の格納場所です。
CRIUは復元時にカーネルから提供されるVVARをそのまま使うので取得しないようです。
vdso(7) - Linux manual page
Implementing virtual system calls [LWN.net]
if (vma_entry_is(vmae, VMA_FILE_PRIVATE) && (pme & PME_FILE)) return false; if (vma_entry_is(vmae, VMA_AREA_AIORING)) return true; if ((pme & (PME_PRESENT | PME_SWAP)) && !__page_is_zero(pme)) return true;
そのページがファイルをマップした領域であれば取得しない。
AIOについては詳しくないのですが、AIOで利用するためにバッファエリアは取得するようです。
そしてそれ以外の部分です。
PME_PRESENTはカーネルから物理ページを与えられている状態、
PME_SWAPはスワップとして利用されている状態です。
そしてこれらのページがゼロページ(書き込みのない状態のページ)であれば取得はしません。
pmeはpagemapの略ですが、Linuxのprocfsにはpagemapという仮想ファイルがあります。
このファイルはそのプロセスの持つ仮想メモリが物理メモリに、どんな状態でどこにあるかというページマップエントリの情報を保持しています。ページマップエントリは一つ8Kで表現されます。procfs/pagemapには仮想メモリの0x0から0x8000000000000000をページで分割した数のページマップエントリがあります。
Pagemap Interface of Linux Explained - Jeff Li
ページマップエントリは上の図のようになっていて、PME_PRESENTは63bit目の値で表現されます。
ゼロページの判定はこのページマップエントリのPageFrameNumerを比較することで行なっています。
ゼロページのPFNは一定になるため、CRIUははじめの方にmmapで新しくメモリを確保し、書き込みを行わず、すぐにprocfs/pagemapで対象のページマップエントリを取得しています。
mem.#87
kerndat.c#409
CRIUのメモリ取得方法
また次回
ioctl FIONREADで取得するサイズの違い
以前書いた記事の最後より
「ただ私が検証してみると、66バイトのデータを送信しているのにも関わらず、ioctlでサイズを取得すると82バイトとなってしまいます。
これがキュー内に新しいデータが入ってしまっていることが原因なのか、型の違いによるものなのか(GoとCの通信をしています)、これが正しい挙動なのか今はわかっていません。」
ioctlで取得するサイズが違うという問題が起きました。
この原因がわかったので記事にします。
結論:ソケットの種類(SOCK_STREAMかSOCK_SEQPACKET)の違い
でした。
int sock = socket(PF_UNIX, SOCK_SEQPACKET, 0);
というように私がやりたかったのは、SOCK_SEQPACKETを使った通信でした。
Go言語
l, _ := net.Listen("unix", "path")
GoでもSOCK_SEQPACKETを使った通信をするコードを書いたつもりでした。
しかし、これは間違いです。
"unix"はSOCK_STREAMに値します。
こちらのサイトに答えがありました。
ASCII.jp:Unixドメインソケット (1/2)|Goならわかるシステムプログラミング
なんと"unix"や"tcp"の他に"unixpacket"というものがあるらしいです。
"unixpacket"がSOCK_SEQPACKETでした。
net - The Go Programming Language
公式ドキュメントにもunixpacketの存在は書いてましたが、SOCK_SEQPACKETであることは書いてなくて、コードまで読まないといけませんでした。
なので、Goの方を
l, _ := net.Listen("unixpacket", "path")
とすれば、同じサイズが取得できました!
VPS for FreeBSDの内部実装 -構造体-
VPS for FreeBSD
よくクラウドとかレンタルサーバーで聞くVPS(Virtual Private Server)とは違います。
Virtual Private System for FreeBSD (link)です。 VPS for FreeBSDは EuroBSDCon2010 で Klaus P. Ohrhallinger が発表したものです。 FreeBSDのコンテナであるJailをライブマイグレーション可能にしたシステムと言われています。 カーネルに手が加えられていますが、競合はあまり起こらないように実装されているらしいです。
今回はコードから見たJailとVPSの違いを、構造体に着目して書いていきたいと思います。 結論から言いますと、VPSはJail実装をベースとして拡張したものです。 Jailに似た部分が多いので、Jailの実装を見た後にVPSを見て行きます。
途中から説明が雑になっていると思います。いい言い回しとか、そもそも間違っているとか、意味わからないとかあったら遠慮なくコメントしてください。
FreeBSD Jailの用語
FreeBSD Jailはjailとかprisonとかいろんな用語をつかいます。 linkから用語をそのまま引用します。
- jail [監獄]
- hosted
- jailシステムで隔離されたシステム、プロセス、サービスを表す。
- host
- jailシステムが動いているシステム、プロセス、サービスを表す。名前の通りの意味。
- prisoner [囚人]
- hostedに同じ
- jailer [看守]
- hostに同じ
コンテナ内部を呼ぶときはhosted/prisoner、コンテナ外部を呼ぶときはhost/jailerと呼びます。 また別の視点(公式のドキュメント)から見ると、 ユーザ空間からコンテナをJailと呼び、カーネル空間からコンテナをPrisonと呼んでいます。 実装レベルでも同じように分けて実装されています。
FreeBSD Jailの実装
まずは、Jailの構造体を見たいと思います。
/* sys/sys/jail.h */ struct jail { uint32_t version; char *path; char *hostname; char *jailname; uint32_t ip4s; uint32_t ip6s; struct in_addr *ip4; struct in6_addr *ip6; };
非常にシンプルですね。jail構造体には、jlsをした時に見える情報、Jailに関する基本的な情報が格納されています。 基本的にはユーザ空間とカーネル空間のインタフェースのような扱いで使われます。 例えば、jail(2)の実行時にカーネルで呼ばれるsys_jailで使われます。
/* sys/kern/kern_jail.c */ int sys_jail(struct thread *td, struct jail_args *uap) { uint32_t version; int error; struct jail j; error = copyin(uap->jail, &version, sizeof(uint32_t)); if (error) return (error); switch (version) { case 0: { struct jail_v0 j0; /* FreeBSD single IPv4 jails. */ bzero(&j, sizeof(struct jail)); error = copyin(uap->jail, &j0, sizeof(struct jail_v0)); if (error) return (error); j.version = j0.version; j.path = j0.path; j.hostname = j0.hostname; j.ip4s = htonl(j0.ip_number); /* jail_v0 is host order */ break; } case 1: /* * Version 1 was used by multi-IPv4 jail implementations * that never made it into the official kernel. */ return (EINVAL); case 2: /* JAIL_API_VERSION */ /* FreeBSD multi-IPv4/IPv6,noIP jails. */ error = copyin(uap->jail, &j, sizeof(struct jail)); if (error) return (error); break; default: /* Sci-Fi jails are not supported, sorry. */ return (EINVAL); } return (kern_jail(td, &j)); }
この関数では、jail(2)の引数をjail構造体にコピーして使っています。 上の方でインタフェースと表現したように、これはコンテナの実体ではありません。
次に、FreeBSDにおけるコンテナの実体であるprison構造体を見ます。
/* sys/sys/jail.h */ struct prison { TAILQ_ENTRY(prison) pr_list; /* (a) all prisons */ int pr_id; /* (c) prison id */ int pr_ref; /* (p) refcount */ int pr_uref; /* (p) user (alive) refcount */ unsigned pr_flags; /* (p) PR_* flags */ LIST_HEAD(, prison) pr_children; /* (a) list of child jails */ LIST_ENTRY(prison) pr_sibling; /* (a) next in parent's list */ struct prison *pr_parent; /* (c) containing jail */ struct mtx pr_mtx; struct task pr_task; /* (c) destroy task */ struct osd pr_osd; /* (p) additional data */ struct cpuset *pr_cpuset; /* (p) cpuset */ struct vnet *pr_vnet; /* (c) network stack */ struct vnode *pr_root; /* (c) vnode to rdir */ int pr_ip4s; /* (p) number of v4 IPs */ int pr_ip6s; /* (p) number of v6 IPs */ struct in_addr *pr_ip4; /* (p) v4 IPs of jail */ struct in6_addr *pr_ip6; /* (p) v6 IPs of jail */ struct prison_racct *pr_prison_racct; /* (c) racct jail proxy */ void *pr_sparep[3]; int pr_childcount; /* (a) number of child jails */ int pr_childmax; /* (p) maximum child jails */ unsigned pr_allow; /* (p) PR_ALLOW_* flags */ int pr_securelevel; /* (p) securelevel */ int pr_enforce_statfs; /* (p) statfs permission */ int pr_devfs_rsnum; /* (p) devfs ruleset */ int pr_spare[3]; int pr_osreldate; /* (c) kern.osreldate value */ unsigned long pr_hostid; /* (p) jail hostid */ char pr_name[MAXHOSTNAMELEN]; /* (p) admin jail name */ char pr_path[MAXPATHLEN]; /* (c) chroot path */ char pr_hostname[MAXHOSTNAMELEN]; /* (p) jail hostname */ char pr_domainname[MAXHOSTNAMELEN]; /* (p) jail domainname */ char pr_hostuuid[HOSTUUIDLEN]; /* (p) jail hostuuid */ char pr_osrelease[OSRELEASELEN]; /* (c) kern.osrelease value */ };
大きめな構造体になっていますね。prison構造体はリストになっています。
pr_childcountやpr_childmaxといったコンテナのネスト数なども管理してます。
メンバ変数のpr_prison_racctは、hosted/prisonerが使っているリソース(cpuやメモリなど)の情報を含んでおり、主にRACCT/RCTLで使われます。
prison構造体は、スレッドが保持するucred構造体に格納され、td->td_ucred->pr_prison
という使われ方をします。
host/jailer(コンテナホスト)のスレッドのpr_prisonには特別な値prison0が代入されます。prison0はゼロ値(っぽい値)が入ったprison構造体です。
/* sys/sys/kern_jail.c */ struct prison prison0 = { .pr_id = 0, .pr_name = "0", .pr_ref = 1, .pr_uref = 1, .pr_path = "/", .pr_securelevel = -1, .pr_devfs_rsnum = 0, .pr_childmax = JAIL_MAX, .pr_hostuuid = DEFAULT_HOSTUUID, .pr_children = LIST_HEAD_INITIALIZER(prison0.pr_children), #ifdef VIMAGE .pr_flags = PR_HOST|PR_VNET|_PR_IP_SADDRSEL, #else .pr_flags = PR_HOST|_PR_IP_SADDRSEL, #endif .pr_allow = PR_ALLOW_ALL_STATIC, };
ここまでのFreeBSD Jailの情報をまとめてみます。
- jail構造体はユーザ空間とカーネル空間とのインタフェースのような扱い
- コンテナの実体はprison
- スレッドごとにprison構造体を持つ
- コンテナホストのスレッドはprison0が代入される
FreeBSD VPSの実装
まず、vps構造体を見ましょう。
/* sys/vps/vps2.h */ struct vps { struct vnet *vnet; LIST_ENTRY(vps) vps_all; LIST_ENTRY(vps) vps_sibling; LIST_HEAD(, vps) vps_child_head; struct vps *vps_parent; struct sx vps_lock; char *vps_lock_name; u_int vps_id; char vps_name[MAXHOSTNAMELEN]; u_char vps_status; u_int vps_refcnt; struct mtx vps_refcnt_lock; #ifdef INVARIANTS TAILQ_HEAD(, vps_ref) vps_ref_head; #endif struct timeout_task vps_task; u_char priv_allow_set[PRIV_SET_SIZE]; u_char priv_impl_set[PRIV_SET_SIZE]; struct vps_arg_ip4 *vps_ip4; struct vps_arg_ip6 *vps_ip6; u_int16_t vps_ip4_cnt; u_int16_t vps_ip6_cnt; u_int vps_flags; int restore_count; int64_t suspend_time; struct vps_acc *vps_acc; /* XXX do inline */ struct vnode *consolelog; struct tty *console_tty; struct file *console_fp_ma; int consolelog_refcnt; int console_flags; struct ucred *vps_ucred; struct devfs_rule *devfs_ruleset; struct vnode *_rootvnode; char _rootpath[MAXPATHLEN]; };
prison構造体と似た作りになっていますが、違う情報も扱っていることがわかります。 prison構造体とは全く関係がないのかというとそうではなくて、ucred構造体であるvps_ucredを持っています。 上のprison構造体の説明で書きましたが、ucred構造体はprison構造体を含んでいます。 vps構造体はprison構造体を含んでいる。つまり、vps構造体はprison構造体の拡張になります。
/* sys/vps/vps_core.c */ struct vps * vps_alloc(struct vps *vps_parent, struct vps_param *vps_pr, char *vps_name, int *errorval) { struct vps *vps; struct vps *vps_save; struct nameidata nd; char *path; int error; char *tmpstr = NULL; struct thread *td = curthread; ~~~~~~~~~中略~~~~~~~~~~ vps_prison_alloc(vps_parent, vps); prison_hold(VPS_VPS(vps, prison0)); prison_free(vps->vps_ucred->cr_prison); vps->vps_ucred->cr_prison = VPS_VPS(vps, prison0); ~~~~~~~~~~~~~~~~~~~~~~ } static int vps_prison_alloc(struct vps *vps_parent, struct vps *vps) { struct prison *pp, *np; ~~~~~~~~~中略~~~~~~~~~~ VPS_VPS(vps, prison0) = malloc(sizeof(struct prison), M_VPS_CORE, M_WAITOK|M_ZERO); np = VPS_VPS(vps, prison0); pp = VPS_VPS(vps_parent, prison0); ~~~~~~~~~~~~~~~~~~~~~~ }
vps_alloc関数はvpsctlでコンテナを実行した後、vps構造体を作る関数です。 vpsを作る中で、vps_prison_alloc関数を呼び、prison構造体を作成しています。
/* sys/vps/vps_core.c */ static void vps_init(void *unused) { LIST_INIT(&vps_head); /* init all the global locks here */ sx_init(&vps_all_lock, "lock of all vps instances"); sx_xlock(&vps_all_lock); vps0 = vps_alloc(NULL, NULL, "", NULL); sx_xunlock(&vps_all_lock); curthread->td_vps = vps0; vps_console_init(); DBGCORE("WARNING: VPS VIRTUAL PRIVATE SYSTEMS ENABLED. " "HIGHLY EXPERIMENTAL!\n"); }
OS起動時にはhost/jailerに対して、ゼロ値で初期化されたvps構造体を作成しています。
prison0も一緒に作成されますが、VPSの場合はtd->td_vps->vps_ucred->cr_prison
に代入されます。
ここまでのVPS for FreeBSDの情報をまとめてみます。
まとめ
FreeBSD JailとVPS for FreeBSDでは、似ている部分としてprisonを使ってコンテナを作成していることが挙げられます。 しかし、jailはユーザ空間とカーネル空間のインタフェース的な存在なのに対し、vpsはprisonそのもののような役割を持っています。
FreeBSDのrecv(2)がこれから受け取るであろうデータのサイズを取得する
2018/11/22時点
OSSであるCRIUのコードを読んでいて、これからrecv(2)で取得するデータのサイズがわからないから、一度データサイズを取得してからバッファを確保するという処理に出会いました。
Linuxの場合
以下のコードはLinuxで実行すると受け取ろうとするデータのサイズを取得することができる。
int len; len = recv(fd, NULL, 0, MSG_TRUNC | MSG_PEEK); printf("size: %d\n", len);
MSG_TRUNC (since Linux 2.2) For raw (AF_PACKET), Internet datagram (since Linux 2.4.27/2.6.8), netlink (since Linux 2.6.22), and UNIX datagram (since Linux 3.4) sockets: return the real length of the packet or datagram, even when it was longer than the passed buffer.
本来であれば、recv(2)の返り値はバッファに格納したサイズになるのだが、 MSG_TRUNCを指定すれば、パケットやデータグラムの本当の長さを返すようにできる。
FreeBSDの場合
Linuxと同じコードを実行してもlen=0になる。 FreeBSD man recv(2)より
The msg_flags field is set on return according to the message received. MSG_EOR indicates end-of-record; the data returned completed a record (generally used with sockets of type SOCK_SEQPACKET). MSG_TRUNC indi- cates that the trailing portion of a datagram was discarded because the datagram was larger than the buffer supplied. MSG_CTRUNC indicates that some control data were discarded due to lack of space in the buffer for ancillary data. MSG_OOB is returned to indicate that expedited or out- of-band data were received.
MSG_TRUNCに関する部分を抜粋すると
MSG_TRUNC indicates that the trailing portion of a datagram was discarded because the datagram was larger than the buffer supplied.
バッファが足りない部分は破棄されたことを示すとあります。 よく文章を読んでみると
The msg_flags field is set on return according to the message received.
FreeBSDにおいては、MSG_TRUNCは受け取り専用のパラメータであり、recvのオプションとしてユーザランドから指定して使うことはできないようです。 一応バグとして報告されたこと(link)もありました。
また、FreeBSDのLinux互換レイヤLinuxulatorでもこのMSG_TRUNCは互換されている(link)ようですが、recv(2)のMSG_TRUNCは同じ挙動はしないことがわかったので、 Linux ELF on FreeBSDだとしても、MSG_TRUNCを使ってサイズを取得することはできないと思います。
もしも、受け取るデータのサイズを取得したい場合には、ioctl(2)のFIONREADを使うと思います。
ただ私が検証してみると、66バイトのデータを送信しているのにも関わらず、ioctlでサイズを取得すると82バイトとなってしまいます。 これがキュー内に新しいデータが入ってしまっていることが原因なのか、型の違いによるものなのか(GoとCの通信をしています)、これが正しい挙動なのか今はわかっていません。