自宅のマシンがこわれた

先週末、自宅の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)にマッチするドライバがみつからないためと思われます。

で、その原因として考えられることは、

くらいかなあと思うので、カーネルが起動時に 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 にトベ

さらに、自分でモジュールを書くならば、このへんで苦しんでいた問題も解決しそうな感じもします。
モジュールが出来次第、どこか(?)で公開してみたいと思います。