自宅のマシンがこわれた
先週末、自宅のPCが「カカカカカカッ」と異音をだして動かなくなりました。
いつ買ったかも覚えていない60Gのディスクなので、まあしゃあないかなあと思いつつ、「ぱそこんくんがポンポンイタイタで泣いてるので買い物してくるね」と、かみさんと子供たちに言い残し、近所の大型の電気屋さんに向かいました。やっぱ最近はほとんどがSATAなんすねえ・・・手に届くところに並んでいるのはすべてSATA。
店員さんに「3.5インチのぱられるATAでいっちゃん容量でかいの下さいな」というと、どこぞから脚立を持ってきて棚の一番上のほうをごそごそしたのち、320Gのディスクを出してきてくれました。このディスクが壊れたらいい加減本体を買い替えようかな。
カーネルモジュールことはじめ #7
今日は ip_vs_test_schedule() 関数に手をいれます。(コードはこちら)
この関数は、IPVSが新しい接続を受け入れる時に呼び出されます。そして接続すべきリアルサーバを指す ip_vs_dest 構造体のポインタを返します。リアルサーバのリストは、引数で渡される ip_vs_service 構造体の destinations に格納されています。このコードでは、リストの先頭から以下の条件を満たすサーバを検索します。
- weightが0じゃないサーバ
- 接続数がしきい値(u_threshold)を越えていないサーバ
動作確認
# ipvsadm -A -t 10.211.55.3:21 -s test # ipvsadm -a -t 10.211.55.3:21 -r 10.211.55.5:21 -x 4 -y 1 # ipvsadm -a -t 10.211.55.3:21 -r 10.211.55.6:21 -x 4 -y 1 # ipvsadm -a -t 10.211.55.3:21 -r 10.211.55.7:21 -x 4 -y 1 # ipvsadm -L -n --thresholds IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Uthreshold Lthreshold ActiveConn InActConn -> RemoteAddress:Port TCP 10.211.55.3:21 test -> 10.211.55.253:21 4 1 0 0 -> 10.211.55.252:21 4 1 0 0 -> 10.211.55.251:21 4 1 0 0 -> 10.211.55.250:21 4 1 0 0
接続確認
client:$ telnet 10.211.55.3 21 Connected to 10.211.55.3. Escape character is '^]'. 220 ProFTPD 1.3.0 Server (Debian) [10.211.55.253]
これを5個動かすとIPVSの状態は以下のようになります。
# ipvsadm -L -n --thresholds IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Uthreshold Lthreshold ActiveConn InActConn -> RemoteAddress:Port TCP 10.211.55.3:21 test -> 10.211.55.253:21 4 1 5 0 -> 10.211.55.252:21 4 1 0 0 -> 10.211.55.251:21 4 1 0 0 -> 10.211.55.250:21 4 1 0 0
リストの先頭のサーバに5セッション張られました。
さらにもうひとつ接続すると、、
# ipvsadm -L -n --thresholds IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Uthreshold Lthreshold ActiveConn InActConn -> RemoteAddress:Port TCP 10.211.55.3:21 test -> 10.211.55.253:21 4 1 5 0 -> 10.211.55.252:21 4 1 1 0 -> 10.211.55.251:21 4 1 0 0 -> 10.211.55.250:21 4 1 0 0
今度は2番目のサーバに繋ぎにいきました。
では、一番最初に起動した telnet をいったん止めて再接続するとどうなるでしょう。
# ipvsadm -L -n --thresholds IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Uthreshold Lthreshold ActiveConn InActConn -> RemoteAddress:Port TCP 10.211.55.3:21 test -> 10.211.55.253:21 4 1 4 1 -> 10.211.55.252:21 4 1 2 0 -> 10.211.55.251:21 4 1 0 0 -> 10.211.55.250:21 4 1 0 0
接続が切れても 10.211.55.253 は OverLoadのままなので 10.211.55.252 に繋がります。
どうやら想定通りの動きをしてくれているようです。
ソースコード(ip_vs_test.c)
#include <linux/module.h> #include <linux/kernel.h> #include <net/ip_vs.h> static int ip_vs_test_init_svc(struct ip_vs_service *svc) { IP_VS_DBG(7, "ip_vs_test: init_svc\n"); return 0; } static int ip_vs_test_done_svc(struct ip_vs_service *svc) { IP_VS_DBG(7, "ip_vs_test: done_svc\n"); return 0; } static int ip_vs_test_update_svc(struct ip_vs_service *svc) { IP_VS_DBG(7, "ip_vs_test: update_svc\n"); return 0; } static struct ip_vs_dest * ip_vs_test_schedule(struct ip_vs_service *svc, const struct sk_buff *skb) { struct ip_vs_dest *dest; list_for_each_entry(dest, &svc->destinations, n_list){ if(atomic_read(&dest->weight)){ if(dest->flags & IP_VS_DEST_F_OVERLOAD){ /* OverLoad */ }else{ return(dest); } } } return(NULL) ; } static struct ip_vs_scheduler ip_vs_test_scheduler = { .name = "test", .refcnt = ATOMIC_INIT(0), .module = THIS_MODULE, .init_service = ip_vs_test_init_svc, .done_service = ip_vs_test_done_svc, .update_service = ip_vs_test_update_svc, .schedule = ip_vs_test_schedule, }; static int __init ip_vs_test_init(void) { INIT_LIST_HEAD(&ip_vs_test_scheduler.n_list); return register_ip_vs_scheduler(&ip_vs_test_scheduler) ; } static void __exit ip_vs_test_cleanup(void) { unregister_ip_vs_scheduler(&ip_vs_test_scheduler); } module_init(ip_vs_test_init); module_exit(ip_vs_test_cleanup); MODULE_LICENSE("GPL");
デバイスドライバロード時の動作
どういう訳か、見えない「何か」に背中を押されたので、Linuxがデバイスドライバをロードした時の挙動を追ってみようと思います。例として(?) # modprobe r8169 した場合の挙動を追うことにします。
# 少し長めなので結論を急ぐ方は こちら をどうぞ。
まずは drivers/net/r8169.c の rtl8169_init_module() が呼ばれるところからスタートします。
static int __init rtl8169_init_module(void) { return pci_register_driver(&rtl8169_pci_driver); } static void __exit rtl8169_cleanup_module(void) { pci_unregister_driver(&rtl8169_pci_driver); } module_init(rtl8169_init_module); module_exit(rtl8169_cleanup_module);
ここでやっていることは pci_register_driver() だけですね。
で、引数として渡している &rtl8169_pci_driver はこんな構造体です。
struct pci_driver { struct list_head node; char *name; const struct pci_device_id *id_table; int (*probe) (struct pci_dev *dev, const struct pci_device_id *id); void (*remove) (struct pci_dev *dev); int (*suspend) (struct pci_dev *dev, pm_message_t state); int (*suspend_late) (struct pci_dev *dev, pm_message_t state); int (*resume_early) (struct pci_dev *dev); int (*resume) (struct pci_dev *dev); int (*enable_wake) (struct pci_dev *dev, pci_power_t state, int enable); void (*shutdown) (struct pci_dev *dev); struct pci_error_handlers *err_handler; struct device_driver driver; struct pci_dynids dynids; int multithread_probe; };
と、いろいろありますが、とりあえず今回注目するメンバは以下のふたつです。
id_table
このドライバがサポートするデバイスのVendorIDとDeviceIDの組を保持するバッファのポインタです。
r8139.cでは以下のデバイスをサポートしています。
10EC:8129 # Realtek: RTL-8129 10EC:8136 # Realtek: RTL8101E PCI Express Fast Ethernet controller 10EC:8167 # Realtek: RTL-8110SC/8169SC Gigabit Ethernet 10EC:8168 # Realtek: RTL8111/8168B PCI Express Gigabit Ethernet controller 10EC:8169 # Realtek: RTL-8169 Gigabit Ethernet 1186:4300 # D-Link System Inc: DGE-528T Gigabit Ethernet Adapter 1259:C107 # Allied Telesyn International製のなんか 16EC:0116 # U.S. Robotics: USR997902 10/100/1000 Mbps PCI Network Card 1737:1032 # Linksys: Gigabit Network Adapter
(*probe)(struct pci_dev *dev, const struct pci_device_id *id);
デバイスを初期化するために呼び出す関数のポインタ。
.probe = rtl8169_init_one
さて、それでは、pci_register_driver() の処理はどうなっているのでしょうか。
include/linux/pci.h では以下のようになっています。
static inline int __must_check pci_register_driver(struct pci_driver *driver) { return __pci_register_driver(driver, THIS_MODULE); }
本体の __pci_register_driver() は drivers/pci/pci-driver.c にあります。
int __pci_register_driver(struct pci_driver *drv, struct module *owner) { int error; /* initialize common driver fields */ drv->driver.name = drv->name; drv->driver.bus = &pci_bus_type; drv->driver.owner = owner; drv->driver.kobj.ktype = &pci_driver_kobj_type; if (pci_multithread_probe) drv->driver.multithread_probe = pci_multithread_probe; else drv->driver.multithread_probe = drv->multithread_probe; spin_lock_init(&drv->dynids.lock); INIT_LIST_HEAD(&drv->dynids.list); /* register with core */ error = driver_register(&drv->driver); if (!error) error = pci_create_newid_file(drv); return error; }
で、最後までまじめに追うと終わらないので少し飛ばします。
結局のところ、どこぞからか drivers/pci/pci-driver.c の pci_device_probe() が呼ばれ、pci_match_device() と pci_match_id() を経て、
static int __pci_device_probe(struct pci_driver *drv, struct pci_dev *pci_dev) { const struct pci_device_id *id; int error = 0; if (!pci_dev->driver && drv->probe) { error = -ENODEV; id = pci_match_device(drv, pci_dev); if (id) error = pci_call_probe(drv, pci_dev, id); if (error >= 0) { pci_dev->driver = drv; error = 0; } } return error; } static int pci_device_probe(struct device * dev) { int error = 0; struct pci_driver *drv; struct pci_dev *pci_dev; drv = to_pci_driver(dev->driver); pci_dev = to_pci_dev(dev); pci_dev_get(pci_dev); error = __pci_device_probe(drv, pci_dev); if (error) pci_dev_put(pci_dev); return error; }
最終的には drivers/pci/pci.h の pci_match_one_device() で id_tableに登録されているIDとハードウエアのIDが一致するかどうかを確認できたら、初期関数である rtl8169_init_one() を pci_call_probe() で呼び出しているんだと思います・・・
static inline const struct pci_device_id * pci_match_one_device(const struct pci_device_id *id, const struct pci_dev *dev) { if ((id->vendor == PCI_ANY_ID || id->vendor == dev->vendor) && (id->device == PCI_ANY_ID || id->device == dev->device) && (id->subvendor == PCI_ANY_ID || id->subvendor == dev->subsystem_vendor) && (id->subdevice == PCI_ANY_ID || id->subdevice == dev->subsystem_device) && !((id->class ^ dev->class) & id->class_mask)) return id; return NULL; }
まとめ?
というわけで、もし仮に lspci とかで「desc: "Unknown device 0001:8168"」と言われてデバイスを認識できないようなことがあったとしたら、それは未定義なベンダーID(0x0001)にマッチするドライバがみつからないためと思われます。
で、その原因として考えられることは、
- 工場出荷時のミス(ほんと?
- NICが壊れた(まじ?
- NICのファームがおかしい(おいおい!
- BIOSでNICがDisableになってる(あるかも?
- LinuxカーネルがPCIデバイスのスキャンに失敗してる(むぅ
- カルボナーラが食べたい(w
くらいかなあと思うので、カーネルが起動時に pci_read_config_xxx() で変な値を読み込んでしまっているのか、もしくはNICのコンフィグレーションレジスタに VenderID=0x0001 と本当に書き込まれてしまっているのかを切り分けてみるのも有効かと思われます。
今回の教訓
すごい人を下手にいぢると痛い目をみるお!
カーネルモジュールことはじめ #5
ip_vs_test 用の Kconfig と Makefile のパッチです。これで make menuconfig から test scheduling (IP_VS_TEST) を選択できるようになるはずです。
あ、あと、#3のip_vs_test.c は net/ipv4/ipvs/ に置いて下さい。これで、現在稼働中のカーネルのソースであれば make modules_install するだけで ipvsadm -A -t IP:PORT -s test とかできるようになるんじゃないかな。keepalived.conf にも lb_algo test って書いて動いてくれるはずです。とはいえ、まだ何もしてくれませんが(^^;;
# 昨日のコードに足りない部分があったので追記しました、ごめんなさい。
diff -uN linux-2.6.20.3/net/ipv4/ipvs/Kconfig linux-2.6.20.3.new/net/ipv4/ipvs/Kconfig --- linux-2.6.20.3/net/ipv4/ipvs/Kconfig 2007-03-14 03:27:08.000000000 +0900 +++ linux-2.6.20.3.new/net/ipv4/ipvs/Kconfig 2007-04-09 14:39:57.000000000 +0900 @@ -224,6 +224,12 @@ If you want to compile it in kernel, say Y. To compile it as a module, choose M here. If unsure, say N. +config IP_VS_TEST + tristate "test scheduling" + depends on IP_VS + ---help--- + TEST + comment 'IPVS application helper' depends on IP_VS diff -uN linux-2.6.20.3/net/ipv4/ipvs/Makefile linux-2.6.20.3.new/net/ipv4/ipvs/Makefile --- linux-2.6.20.3/net/ipv4/ipvs/Makefile 2007-03-14 03:27:08.000000000 +0900 +++ linux-2.6.20.3.new/net/ipv4/ipvs/Makefile 2007-04-09 14:29:05.000000000 +0900 @@ -29,6 +29,7 @@ obj-$(CONFIG_IP_VS_SH) += ip_vs_sh.o obj-$(CONFIG_IP_VS_SED) += ip_vs_sed.o obj-$(CONFIG_IP_VS_NQ) += ip_vs_nq.o +obj-$(CONFIG_IP_VS_TEST) += ip_vs_test.o # IPVS application helpers obj-$(CONFIG_IP_VS_FTP) += ip_vs_ftp.o
実行結果
# make modules # make modules_install # ipvsadm -A -t 10.10.0.1:80 -s test # ipvsadm -L -n IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.10.0.1:80 test # lsmod Module Size Used by ip_vs_test 3456 1 ip_vs 97984 5 ip_vs_test
カーネルモジュールことはじめ
見ての通りなので細かい説明しません、ごめんなさい(ちょー手抜き!
hello.c
#include <linux/module.h> static void __exit hello_exit(void) { printk("hello kernel module unload\n"); } static int __init hello_init(void) { printk("hello kernel module!\n"); return 0; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL");
Makefile
obj-m:= hello.o all: hello.ko hello.ko: hello.c make -C /usr/src/linux M=`pwd` V=1 modules clean: make -C /usr/src/linux -r M=`pwd` V=1 clean
実行結果
# make # insmod ./hello.ko # tail /var/log/kern.log May 28 14:18:53 YASUI01 kernel: hello kernel module! # lsmod Module Size Used by hello 1664 0 # rmmod hello # tail /var/log/kern.log May 28 14:18:53 YASUI01 kernel: hello kernel module! May 28 14:19:20 YASUI01 kernel: hello kernel module unload
カーネルモジュールことはじめ #3
どのリアルサーバにも割り当てないIPVSのスケジューラ(!?)
# いったい何に対抗してんだ?俺(^^;;
# 5/29追記、
大事なもの忘れてた!!
register_ip_vs_scheduler() で指定している構造体を定義してないやん!
というわけでコードに追記しました。試してみた人(もしいたら)ごめんなさい。
ip_vs_test.c
#include <linux/module.h> #include <linux/kernel.h> #include <net/ip_vs.h> static int ip_vs_test_init_svc(struct ip_vs_service *svc) { IP_VS_DBG(7, "ip_vs_test: init_svc\n"); return 0; } static int ip_vs_test_done_svc(struct ip_vs_service *svc) { IP_VS_DBG(7, "ip_vs_test: done_svc\n"); return 0; } static int ip_vs_test_update_svc(struct ip_vs_service *svc) { IP_VS_DBG(7, "ip_vs_test: update_svc\n"); return 0; } static struct ip_vs_dest * ip_vs_test_schedule(struct ip_vs_service *svc, const struct sk_buff *skb) { IP_VS_DBG(7, "ip_vs_test_schedule(): Scheduling...\n"); return 0; } /*----- 5/29追記 -----*/ static struct ip_vs_scheduler ip_vs_test_scheduler = { .name = "test", .refcnt = ATOMIC_INIT(0), .module = THIS_MODULE, .init_service = ip_vs_test_init_svc, .done_service = ip_vs_test_done_svc, .update_service = ip_vs_test_update_svc, .schedule = ip_vs_test_schedule, }; /*----- ここまで -----*/ static int __init ip_vs_test_init(void) { /*----- 5/29追記 -----*/ INIT_LIST_HEAD(&ip_vs_test_scheduler.n_list); /*----- ここまで -----*/ return register_ip_vs_scheduler(&ip_vs_test_scheduler) ; } static void __exit ip_vs_test_cleanup(void) { unregister_ip_vs_scheduler(&ip_vs_test_scheduler); } module_init(ip_vs_test_init); module_exit(ip_vs_test_cleanup); MODULE_LICENSE("GPL");
とりあえず最低限必要なのはこんだけ。
スマートに動かすには net/ipv4/ipvs/Kconfig と net/ipv4/ipvs/Makefile を書き換えるのが楽かな。
とりあえずはこれで
# ipvsadm -A -t 10.0.0.1:80 -s test
とかできるようになるかと・・・
IPVSでthresholdを有効活用したい
かなり時間が空いてしまった感がありますが、IPVSのthresholdネタです。
こないだは、既存のスケジューラに小細工をして、weight=1 なサーバを sorry_server と見なして動かしてみました。
しかし、これだと、
- 全スケジューラのソースに手を加えないといかん
- weight=1がsorry_serverって定義自体いまいちピンとこない
- カーネルのバージョンアップについてくのが大変そう
な問題があります。
なら「自分でIPVSのスケジューラを作ればいいんじゃん!」ってことで、軽く書いてみようと思います。
方針はこんな感じ
- u_thresholdの設定値が高いサーバを優先して接続する
- u_threshold=0なサーバをsorry_serverとみなす
ということで、weight の代わりに u_threshold で重み付けをし、u_threshold=0 なサーバには通常は接続せず、
u_threshold>0 なサーバのコネクション数がいっぱいになったら u_threshold=0 なサーバへ接続する。
こんなスケジューラがあってもいいかなと思いました。
設定のイメージはこんな感じ
virtual_server 10.0.0.1 80 { delay_loop 5 lb_algo th ← 今回作るスケージューラの仮の名前(なんかいい名前かねえ lb_kind DR protocol TCP # sorry server real_server 192.168.0.1 80{ inhibit_on_failure uthreshold 0 lthreshold 0 } # web server real_server 192.168.0.2 80 { weight 1 inhibit_on_failure uthreshold 300 lthreshold 0 } }
そんで期待する動作はこんな感じ、
モシ(同時接続数 <= 300) ナラバ 192.168.0.2 にトベ モシ(同時接続数 > 300) ナラバ 192.168.0.1 にトベ
さらに、自分でモジュールを書くならば、このへんで苦しんでいた問題も解決しそうな感じもします。
モジュールが出来次第、どこか(?)で公開してみたいと思います。