搜尋此網誌

2012年6月13日 星期三

[個人意見] 從食品檢驗到資安驗證,是向上提昇還是向下沈淪?

最近這一陣子,社會各界為了是否開放美牛而爭論不休。比較理性的一派,認為只要做好檢驗的工作,就可以控制開放美牛的風險。狂牛症我不熟,對於食品的安全我也是門外漢,但是剛好這幾天看到天下雜誌的這篇文章 - 《畜產專家也敢吃的好肉好蛋》如何當個畜產品的消費高手、買到好東西?,裡面的專家提到下列幾段話:


檢驗科技有限制,對黑心仍防不勝防。...


許多消費者一直以為食品通過檢驗就是安全,也是一個錯誤觀念。我必須強調食物生產過程中,有沒有具備一個有效的安全管制系統很重要,如果沒有一套系統確保食物原料的來源、規格與批次一致,那麼檢驗合格只代表那一個當初被檢驗的樣品合格,不代表整批產品都合格。...


此外,我們常常看電視演出,拿什麼證物去檢驗就可以驗出所有的成分,那是騙人的!各位消費者也許不知道,檢驗是要有目標的。這個世界上的化學物質那麼多,哪裡驗得完?而檢驗一項成分所需要的費用從幾百到幾千塊不等,所以一般都是會挑選懷疑的、常見的或慣例檢驗的項目來檢驗,否則驗沒幾次食品公司就要倒閉了!...

這些跟資訊安全有什麼關係?有的,因為資安也有一些所謂的驗證標準 (如 ISO 27001),而這些標準也面臨跟上述食品檢驗類似的問題與困境。這些問題包含:

  1. 驗證僅能針對一些列在其中的項目進行檢查,但是資訊安全是一個全面的議題,任何一個小地方的疏忽都有可能造成莫大的損失。
  2. 驗證項目的檢查有其準確性與完整性。通常檢驗對雙方來說都是一個高成本的活動,所以不可能鉅細靡遺的一一確認,所以就算屬於驗證的項目,也會有其不足的地方。
  3. 驗證檢查的是某個時間點的靜止狀態,而資訊安全卻是一個無時無刻都無法鬆懈的活動。今天這樣的作法或許可以提供足夠的安全,但是並不確保明天也將是一樣的安全。
當然,訂定驗證標準的專家們也都知道上述的問題,所以會要求組織必須提供完整的管理辦法,並加上定期檢視的條款,以希望將資訊安全變成組織日常作業的一環。但是嚴格來看,這些要求本身同樣會面臨上述的問題,所以其實並沒有辦法真正地解決問題。

這麼說來,這些驗證的標準一點用處也沒有了嗎?倒也不用這麼悲觀。驗證標準對於有心於資訊安全的組織,確實提供了一個很好的參考與落實方向,所以絕對有其存在的必要性。而且在某種程度上,驗證也是外界用來評估一個不熟悉事物(組織)時"比較公正"的方式。但是不可輕忽的是,對於那些只想便宜行事的組織,驗證確實也成了他們用來掩護自身不足的最佳手段。

記得兩三年前我有一次榮幸的機會跟一群資安專家介紹 ASVS 這個驗證標準,在台上我向這群專家們提問了"導入資訊安全相關的驗證標準可以提昇還是降低組織資訊安全的程度?"這個看似簡單不過的問題。長久以來,我們一直告訴自己跟客戶,做了才安全,所以這個問題的答案不是應該很明顯嗎?

但是仔細想想,真的是這樣嗎?我們從小到大都應該有無數次的考試經驗,而這些經驗告訴我們,考試考的好的人不一定是準備最充分的那些人。考試考的好,除了準備充分這個理由之外,可能還有考題太簡單、考運好、很會準備考試、助教剛好是你室友的男朋友等種種原因。而驗證嘛,也可以直接套用這些經驗。但是你說考試不好嗎?至少目前社會上很多聲音還是認為考試是"比較公平"的評量方式。所以我當天的說法是「不一定,最終還是取決在組織的心態與作法上。如果確實是為了提昇組織本身的資訊安全水準,導入並通過這些驗證確實會有很大的幫助。但是如果只是為了驗證而驗證,那就跟為了考試而考試一樣。組織將只專注於如何通過驗證,反而無心於解決真正的資訊安全問題,那麼整體的資訊安全水準不但不會提昇,甚至會嚴重下降。

最近吵的沸沸揚揚的新版個資法,雖然並不是一個驗證標準,但是也會面臨同樣的問題。這麼多因個資法而開始嘗試保護個資的組織,是真的想要做好個資的保護,還是只想在法律前獲得免責的機會?背後的動機將決定個資法對該組織的資訊安全水準是向上提昇還是反而促使向下沈淪了。

2012年6月8日 星期五

[迷你好兔] 利用 syslog/rsyslog + 腳本即時監測 Linux 帳號的登入

login_large之前我提過如何利用 Fail2Ban 來避免系統遭受暴力破解的危害,而 Fail2Ban 與其他類似的機制大多著眼於錯誤的行為 (如輸入密碼錯誤)。但是在系統運作的過程中,不但這些錯誤的行為必須加以注意,很多看似"正常"的行為,更是輕忽不得。其中一種絕對不可忽視的行為,就是帳號的登入,尤其是所謂特權帳號 (root 或是 administrator) 的登入。特權帳號的登入,是一個很重要的行為,而成功的登入甚至比錯誤登入更必須加以注意。如果沒有做好相關的監測,哪天忽然發現系統多了一些莫名其妙的檔案,也沒什麼好大驚小怪的了。

有一些事後日誌的分析工具,可以針對這些成功登入的事件加以設定並提出報告。但是事後分析再好,終究無法挽回已經發生的遺憾。如果能夠在第一時間就知道登入的行為,就可以減少遺憾發生的機會。據我所知,目前並沒有很方便的小工具可以達到這個需求,所以我們只好自己動手。有不少方式可以完成這個目標,今天我要跟各位分享一個相當簡單的方式,那就是利用 syslog/rsyslog 將訊息送到 named pipe 的功能,然後撰寫一個簡單的腳本將相關訊息從 named pipe 中讀出並透過電子郵件即時通知管理者

我今天選擇的環境是 CentOS 6.2,至於其他的環境,只要依據實際的狀況做調整就可以了。好了,馬上就開始我們今天的目標吧:
  1. 建立 named pipe
    指令是
    mkfifo /var/run/secure.pipe

  2. 限制 named pipe 的權限
    將群組與全域的權限予以關閉,指令是
    chmod og-rwx /var/run/secure.pipe

  3. 修改 syslog/rsyslog 的設定檔
    CentOS 6.2 內建的套件是 rsyslog,其設定檔是 /etc/rsyslog.conf 。打開檔案後找到相關位置並加上最後一行
    # The authpriv file has restricted access.
    authpriv.*                                              /var/log/secure
    authpriv.*                                              |/var/run/secure.pipe

    如果你的環境使用 syslog 套件,設定上也沒有多大的差別,只要找到相關的設定檔即可。

  4. 重新啟動 syslog/rsyslog 服務
    指令是
    service rsyslog restart 

  5. 撰寫腳本
    建立一個名稱為 monitor.pl 的腳本,內容如下
    #!/usr/bin/perl
    
    my $named_pipe = '/var/run/secure.pipe';
    my $sender = 'SWatchDog<cyril.hcwang@gmail.com>';
    my $receiver = 'cyril.hcwang@gmail.com';
    my @ip_whitelist = ('127.0.0.1', '192.168.1.1');
    
    open (SYSLOG, "+<$named_pipe")
        or die "Couldn't open $named_pipe: $!\n";
    
    while (<SYSLOG>) {
        if ($_ =~ m/(.*) (\S*) sshd\[(\d*)\]: Accepted (\S*) for (\S*) from (\S*)/) {
            if (!ip_in_whitelist($6)) {
                $message = <<END;
    $5 在 $1 從 $6 登入 $2;
    END
                $subject = "[SWatchDog] Session activation report for ".$2;
                print "sending email to ".$receiver." with message \"".$message."\"";
                &email($subject, $message);
            }
        }
    }
    
    close (SYSLOG);
    
    sub email
    {
        ($subject, $message) = @_;
        open(SENDMAIL, "|sendmail -t") or die "Cannot open $sendmail: $!";
        print SENDMAIL "From: ".$sender."\n";
        print SENDMAIL "Subject: ".$subject."\n";
        print SENDMAIL "To: ".$receiver."\n";
        print SENDMAIL "Content-type: text/plain\n\n";
        print SENDMAIL $message;
        close (SENDMAIL);
    }
    
    sub ip_in_whitelist
    {
       ($ip) = @_;
       for ($i=0; $i<=$#ip_whitelist; $i++) {
          if ($ip eq $ip_whitelist[$i]) {
              return 1;
          }
       }
       return 0;
    }
    這個腳本有三個必須注意的地方,第一個當然就是信件通知的對象 ($receiver),另外一個則是哪些 IP 的登入資訊不要通知 (@ip_whitelist) 。最後則是關於訊息的比對指令。 在 CentOS 6.2 上,成功登入的訊息類似如下:
    Jun  1 16:22:38 xxx sshd[28516]: Accepted publickey for root from 127.0.0.1 port 60873 ssh2 
    而比對指令 $_ =~ m/(.*) (\S*) sshd\[(\d*)\]: Accepted (\S*) for (\S*) from (\S*)/ 就是根據這些訊息而撰寫的。如果在你的環境下訊息格式有所不同,可能就必須調整比對的指令了。

  6. 增加執行的權限
    指令是
    chmod +x monitor.pl 

  7. 執行腳本
    指令是
    nohup ./monitor.pl >> /var/log/monitor.log 2>&1 &

    nohup 可以避免我們登出後程式的執行就被中斷。

  8. 嘗試登入系統
    當我們成功登入系統後應該就可以在設定的信箱收到通知訊息,以下是 gmail 信箱的畫面。

    swatchdog

  9. 加入開機自動執行

    我選擇放在 /etc/rc.local 這個檔案,加上下列設定
    /path_to_monitor.pl/monitor.pl >> /var/log/monitor.log 2>&1 
透過 syslog/rsyslog、named pipe 以及 mail,我們輕鬆完成即時監控成功登入系統的事件。這個腳本還有一些可以改進的地方,像是使用其他更即時的通知方式 (msn、簡訊)、提供登入 IP 位址更詳細的資訊(例如這個 IP 位址在哪個地方) 等。只是這種方式雖然簡單,卻有幾個問題必須加以面對,其中一個是每台主機都必須執行這個腳本,在管理上會增加不少負擔。另外一個問題則是腳本一旦停止執行 (或根本沒有執行) 就失去了監測的能力。此外,如果有其他的程式也在讀取這個 named pipe,那麼寫入這個 named pipe 的資料可就不保證會被我們的腳本讀到。所以,named pipe 的權限設定就很重要。

當然,這幾個問題也不是不能解決,只是每台主機自我管理終究還是不夠方便。解決方式之一就是利用 syslog/rsyslog 遠端遞送的功能,把所有相關事件集中到同一台主機,然後在那台主機執行監測的腳本,如此一來就可以減少許多管理的負擔。至於如何將 syslog/rsyslog 事件遞送到遠端,就請有需要的朋友自行參考之前的另外一篇文章了。

2012年6月7日 星期四

[迷你好兔] 啟用 vSphere 5 的 snmp 功能

VMware對於 vSphere 這麼一套應用於大量服務環境下的虛擬化系統,雖然使用 vSphere Client 就可以看到資源的使用狀況,但是缺乏歷史紀錄的功能,讓管理時總覺得缺了那麼些東西。沒關係,我們都知道有 snmp 這個老朋友可以幫助我們主機進行主機或網路設備的管理。幸運的是,vSphere 本身支援 snmp 的功能,只不過預設是關閉的。
不過是啟動個 snmp ,應該是很容易的事情吧?經過一番搜尋後,我查到啟動 vSphere 的 snmp 功能主要有兩種方式,一種是登入 host 後直接修改設定,另外一種則是透過 VMware vSphere Infrastructure Managemenet SDK 從遠端加以設定。二話不說,我當然是選利用 SDK 從遠端加以設定啊,不然一台一台設定多累啊。沒想到這是一個錯誤的選擇,花了我好一陣子才順利的啟動 vSphere 的 snmp 功能,所以也才有這篇文章的出現。

廢話不多說,同樣一步一步來完成今天的目標吧。
  1. 準備一台 Ubuntu 10.4 (或其他支援的系統) 的機器。
    這是卡關的第一步。我一開始採用 CentOS 6.2 的機器,在排除某狀況後,直到出現 uuid 引入檔錯誤 (路徑關係) 的那一刻我就決定放棄 CentOS 6.2,直接安裝一台 Ubuntu 10.4 的機器。反正使用 vSphere 後,什麼不多,就是機器多。我安裝的是 64 位元的 server 版。

  2. 安裝其他需求的套件
    有兩個額外套件需要安裝,分別是 perl-doc 與 uuid-dev。指令是
    apt-get install –y perl-doc 
    apt-get install –y uuid-dev

  3. 下載 vSphere SDK for Perl
    網址是
    http://www.vmware.com/download/download.do?downloadGroup=SDKPERL50U1
    請記得選擇 64 位元的版本。

  4. 解開壓縮檔 指令是
    tar xvfz VMware-vSphere-Perl-SDK-5.0.0-422456.x86_64.gz

  5. 修改 vmware-install.pl
    這是卡關的第二步,安裝程式會檢查是否已經設定 http 與 ftp 的代理伺服器,沒有設定還不給裝。但是現在一般的網路環境應該是不需要設定這些參數,所以我們要修改相關的程式碼。

    指令是
    cd vmware-vsphere-cli-distrib/ 
    vi vmware-install.pl

    將下列程式註解掉或直接刪除都可,註解的方式就是在每行前面加上 # 這個符號。
    if ( direct_command("env | grep -i http_proxy") ) {
        $httpproxy = 1; 
    } else {
        print wrap("http_proxy not set. please set environment variable 'http_proxy' e.g. export http_proxy=http://myproxy.mydomain.com:0000 . \n\n", 0); 
    } 
    if ( direct_command("env | grep -i ftp_proxy") ) {
        $ftpproxy = 1; 
    } else {
        print wrap("ftp_proxy not set. please set environment variable 'ftp_proxy' e.g. export ftp_proxy=http://myproxy.mydomain.com:0000 . \n\n", 0); 
    } 
    
    if ( !( $approx && $httpproxy)) {
        uninstall_file($gInstallerMainDB);
        exit 1; 
    }

  6. 進行安裝
    指令是
    ./vmware-install.pl

    看完版權宣告後請輸入 "yes",開始安裝的準備動作,準備完後會問你要安裝到哪個路徑下。預設是安裝到 /usr/bin 這個路徑,但是我選擇安裝到 /usr/local/bin 這個路徑。

  7. 確認已經正常安裝完畢
    最好的方式就是實際進行 snmp 的操作,我們先來查看 host 的 snmp 設定。指令是
    vicfg-snmp --server host_ip –s

    接著輸入你的帳號跟密碼。
    Enter username: 輸入你的帳號 
    Enter password: 輸入你的密碼 
    Current SNMP agent settings: 
    Enabled  : 0 
    UDP port : 161 
    Communities : 
    Notification targets : 
    Options : 
    EnvEventSource=indications

    看到這些資訊表示你已經連上了 host,而且 snmp 預設是關閉的。

  8. 設定並啟用 snmp
    指令是
    vicfg-snmp --server host_ip -E -c public

    其中 public 是你想要設定的社群 (community) 名稱。建議改成你自己知道的名稱,而不要使用 public 這種菜市場名字。
    Enter username: 輸入你的帳號 
    Enter password: 輸入你的密碼 
    Changing community list to: public... 
    Complete. 
    Enabling agent... 
    Complete.

  9. 再次確認
    指令是
    vicfg-snmp --server host_ip –s
    Enter username: 輸入你的帳號 
    Enter password: 輸入你的密碼
    Current SNMP agent settings: 
    Enabled  : 1 
    UDP port : 161 
    Communities : 
    public

  10. 透過 snmpwalk 讀取 snmp 的資料
    指令是
    snmpwalk –v 2c –c public host_ip

    順利的話,你會看到類似下列的資訊。
    SNMPv2-MIB::sysDescr.0 = STRING: VMware ESX 5.0.0 build-469512 VMware, Inc. x86_64 
    SNMPv2-MIB::sysObjectID.0 = OID: SNMPv2-SMI::enterprises.6876.4.1 
    DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (188729027) 21 days, 20:14:50.27 
    SNMPv2-MIB::sysContact.0 = STRING: not set 
    ...
嚴格說來,除了兩個小卡關點必須注意之外,整個過程並不複雜。如果你很有好奇心,還是可以登入 host (透過 ssh),找到相關的設定檔 (位置是 /etc/vmware/snmp.xml)。裡面的內容應該如下:

<config>
   <snmpSettings>
     <communities>public</communities>
     <enable>true</enable>
     <targets/>
   </snmpSettings> 
</config>

你沒看錯,就是這麼簡單。如果你直接修改這個檔案,記得要執行 services restart 這個指令才會生效。

vSphere 的 snmp 功能在設定很簡單,所以有些進階設定也就無法進行,像是加密、支援不同的view等。此外,snmp 所能提供的資訊也不是很多。vmware 的 snmp 比較強調在 snmp trap 的能力,以提供一個當異常發生時的通知方式。儘管如此,用來當做基本的監控,還是已經足夠了。例如,下圖就是利用 CACTI 來監控 host 的網卡流量:

esxi1

透過 snmp 的支援,可以讓我們把 vSphere 納入現有的網管工具一併加以管理,著實方便多了,不是嗎?

2012年6月6日 星期三

[教戰守則] NoSQL, No Injection?

logo-mongoDB前一陣子在拜讀 MongoDB: The Definitive Guide 這本書時,發現書中有一句很有趣的話:
MongoDB does not do any sort of code execution on inserts, so they are not vulnerable to injection attacks. Traditional injection attacks are impossible with MongoDB, and alternative injection-type attacks are easy to guard against in general, but inserts are particularly invulnerable.

簡單來說,就是使用 MongoDB 後,就算考試沒有 100 分,至少是不用擔心”傳統”的注入攻擊。我不知道”傳統”的定義何在,但是我知道幾乎沒有什麼 data store 是可以完全避免注入攻擊的。SQL如此,LDAP 如此,連 XML 也難逃厄運。到底是什麼理由可以讓 MongoDB 有這麼神奇的能力?通常我看到這種話都是直接嗤之以鼻,但是因為這本書的作者之一是 MongoDB 的核心開發者,而另外一個作者則是 MongoDB 驅動程式的開發者,我怎麼能夠輕忽他們的話呢?

經由一番搜尋後,我找到了 MongoDB 依舊會遭受注入攻擊的證據。我們就用實際的例子來看吧:

首先,我們在測試的資料庫內新增兩筆使用者的資料
[root@mnode2 ~]# mongo 
MongoDB shell version: 1.8.2 
connecting to: test 
myrepl:PRIMARY> use myapp 
switched to db myapp 
myrepl:PRIMARY> db.users.insert({"username":"admin", "password":"1234"}); 
myrepl:PRIMARY> db.users.insert({"username":"guest", "password":"5678"}); 
myrepl:PRIMARY> db.users.find() 
{ "_id" : ObjectId("4fceb7e79ac77b943b49ccf0"), "username" : "admin", "password" : "1234" } 
{ "_id" : ObjectId("4fceb7f09ac77b943b49ccf1"), "username" : "guest", "password" : "5678" }

這兩筆資料可以用來作模擬一般網站常見的登入功能,包含了基本的帳號與密碼。

當使用者登入時,我們會將使用者輸入的帳號與密碼當做搜尋條件,找出使用者擁有的帳號。這個動作在 MongoDB 下,就是如下的查詢方式:
myrepl:PRIMARY> db.users.find({"username":"admin", "password":"1234"}); 
{ "_id" : ObjectId("4fceb7e79ac77b943b49ccf0"), "username" : "admin", "password" : "1234" }

當資料庫傳回資料時,就表示已經通過身分驗證了。但是如果使用者輸入錯誤的密碼 (如12345),則不會傳回任何的資料,也就表示驗證失敗。
myrepl:PRIMARY> db.users.find({"username":"admin", "password":"12345"});

等等,真的是這樣嗎?我們來試試看下列的指令:
myrepl:PRIMARY> db.users.find({"username":"admin", "password":{"$ne":"1"}}); 
{ "_id" : ObjectId("4fceb7e79ac77b943b49ccf0"), "username" : "admin", "password" : "1234" }
是的,我們將密碼改成 {"$ne":"1"} 這個陣列一樣可以查詢到使用者的資料。對很多系統來說,也就表示你已經通過身分驗證了。

在 mongo 的 shell 下如此,那麼對程式而言是否也有如此的可能性?我們用一個 php 程式當做範例。
<?php 
$action = @$_GET['action']; 
if ($action == 'login') {
     try {
         $conn = new Mongo('localhost');
         $db = $conn->myapp;
         $collection = $db->users;
         $criteria = array(
             'username' => $_GET['username'],
             'password' => $_GET['password']
         );
         print_r($criteria);
         $fields = array('username', 'password');
         $cursor = $collection->find($criteria, $fields)->limit(1);
         if ($cursor->count()==1) {
             $obj = $cursor->getNext();
             echo '成功登入<br />';
             echo '帳號: ' . $obj['username'] . '<br/>';
             echo '密碼: ' . $obj['password'] . '<br/>';
             echo '<br/>';
         } else {
             echo '登入失敗<br />';
             echo '帳號: ' . $_GET['username'] . '<br/>';
             echo '密碼: ' . $_GET['password'] . '<br/>';
             echo '<br/>';
        }
        $conn->close();
   } catch (MongoConnectionException $e) {
        die('Error connecting to MongoDB server');
   } catch (MongoException $e) {
        die('Error: ' . $e->getMessage());
   } 
} 
?> 
<form method="GET"> 
<input type='hidden' name='action' value='login'> 
帳號: <input type='text' name='username'><br /> 
密碼: <input type='password' name='password'><br /> 
<input type=submit> 
</form>

你可以乖乖地試著使用正確或錯誤的帳號密碼登入這隻程式,你也可以使用 ?action=login&username=admin&password[$ne]=1 當做連結的參數
mongodb

是的,MongoDB 也會遭受注入攻擊!

解法是什麼?當然還是回到老手法,程式必須做好輸入的檢查(包含型別、內容數值、範圍等)。如果疏於此道,就算是 MongoDB 也無法讓你的系統刀槍不入外加考試 100 分。

2012年6月5日 星期二

[工具介紹] MongoDB Replica Set in CentOS 6.x (下)

logo-mongodb在上一篇文章中,我提到有關 MongoDB Replica Set 的基本概念,在這篇文章中,我們就來實際建置一個可以運作的 Replica Set。今天我採用的環境是 CentOS 6.2,一共準備了三部主機,其 IP 分別是 192.168.1.2 (主機名稱 mnode1)、192.168.1.3 (主機名稱 mnode2) 與 192.168.1.4 (主機名稱 mnode3),其中 192.168.1.2 與 192.168.1.3 分別是標準節點,而 192.168.1.4 則是被動節點。如果你沒有三台主機,也可以在一台機器內執行三個 mongo 資料庫程序來當成是三個節點。值得特別注意的是,雖然官方文件中說明 Replica Set 最小的節點數量是 2 個,但是如果 Replica Set 內只有 2 個節點,一旦原有 Primary 失效後將沒有辦法自動進行 Primary 的切換。原因很簡單,回想一下上一篇的網路失效案例就可以明瞭了。在實際的佈署中,如果無法同時取得三台主機來安裝 MongoDB,可以找一台提供其他服務的機器並指定為仲裁者 (Arbiter) 的節點類型。如此一來不但可以同樣享有自動切換的功能,而且可以減少一台主機的使用。因為仲裁者不會儲存資料,所以不會對原有主機造成過多的額外負擔。

現在,我們就來一步一步完成今天的目標吧。

  1. 在三台主機各自安裝 MongoDB server。 MongoDB 並沒有包含在標準的 CentOS 套件當中,但是可以透過 EPEL 加以取得。除非你有特別的原因,否則請不要自行編譯,直接透過套件方式加以安裝即可。指令為
    yum install –y mongodb-server

  2. 在三台主機設定 mongod 開機後自動啟動。
    指令為
    chkconfig mongod on

  3. 修改第一台主機 (IP 為 192.168.1.2) 的設定檔,設定檔預設位置為 /etc/mongodb.conf。

    bind_ip = 127.0.0.1
    改為
    bind_ip = 127.0.0.1,192.168.1.2
    並加上
    replSet = myrepl/192.168.1.3,192.168.1.4

  4. 修改第二台主機 (IP 為 192.168.1.3) 的設定檔,設定檔預設位置為 /etc/mongodb.conf。

    bind_ip = 127.0.0.1
    改為
    bind_ip = 127.0.0.1,192.168.1.3
    並加上
    replSet = myrepl/192.168.1.2,192.168.1.4

  5. 修改第三台主機 (IP 為 192.168.1.4) 的設定檔,設定檔預設位置為 /etc/mongodb.conf。

    bind_ip = 127.0.0.1
    改為
    bind_ip = 127.0.0.1,192.168.1.4
    並加上
    replSet = myrepl/192.168.1.2,192.168.1.3

  6. 在三台主機分別開啟防火牆。
    MongoDB 預設使用 TCP Port 27017。同樣請記得在三台主機進行相關的設定。

  7. 在三台主機分別啟動 MongoDB 服務。
    指令為
    service mongod start

    此時我們如果查看 MongoDB 的日誌 (預設為 /var/log/mongodb/mongodb.log) 應該會發現類似下面的錯誤訊息:

    Tue Jun  5 17:46:30 [startReplSets] replSet can't get local.system.replset config from self or any seed (EMPTYCONFIG)

    別擔心,這是因為我們還沒有正常的初始化 Replica Set。

  8. 進行 Replica Set 的初始化。
    在第一台主機 (192.168.1.2) 利用指令 mongo 進行 MongoDB 的管理。
    指令為
    > rs.initiate()

    如果此時看到類似下面的訊息,表示 Replica Set 已經初始成功了。

    {
             "info2" : "no configuration explicitly specified -- making one",
             "info" : "Config now saved locally.  Should come online in about a minute.",
             "ok" : 1 
    }

  9. 確認其他主機的狀態。
    此時在其他主機上執行 mongo 指令後,應該會發現提示符號變成
    myrepl:SECONDARY>
    我們可以透過指令 rs.conf() 進一步確認 Replica Set 的設定
    myrepl:SECONDARY>rs.conf() 
    {
             "_id" : "myrepl",
             "version" : 1,
             "members" : [
                     {
                             "_id" : 0,
                             "host" : "192.168.1.2:27017"
                     },
                     {
                             "_id" : 1,
                             "host" : "192.168.1.3"
                     },
                     {
                             "_id" : 2,
                             "host" : "192.168.1.4"
                     }
             ] 
    }

    除了 rs.conf() 之外,我們還可以透過 rs.isMaster() 看到相關的訊息。
    myrepl:SECONDARY> rs.isMaster() 
    {
             "setName" : "myrepl",
             "ismaster" : false,
             "secondary" : true,
             "hosts" : [
                     "192.168.1.3",
                     "192.168.1.4",
                     "192.168.1.2:27017"
             ],
             "primary" : "192.168.1.2:27017",
             "maxBsonObjectSize" : 16777216,
             "ok" : 1 
    }

  10. 將 192.168.1.4 這台主機設定為被動節點。
    回到 Primary 節點 (192.168.1.2),進行下列設定
    myrepl:PRIMARY> var c = rs.conf() 
    myrepl:PRIMARY> c.members[2].priority = 0; 
    0 
    myrepl:PRIMARY> rs.reconfig(c); 
    Tue Jun  5 18:11:54 DBClientCursor::init call() failed 
    Tue Jun  5 18:11:54 query failed : admin.$cmd { replSetReconfig: { _id: "myrepl", version: 3, members: [ { _id: 0, host: "192.168.1.2:27017" }, { _id: 1, host: "192.168.1.3" }, { _id: 2, host: "192.168.1.4", priority: 0.0 } ] } } to: 127.0.0.1 
    shell got exception during reconfig: Error: error doing query: failed 
    in some circumstances, the primary steps down and closes connections on a reconfig 
    null 
    Tue Jun  5 18:11:54 trying reconnect to 127.0.0.1 
    Tue Jun  5 18:11:54 reconnect 127.0.0.1 ok

    再次執行rs.isMaster()可以發現設定值的改變,其中 192.168.1.4 這台主機已經變成被動 (Passive) 節點:
    myrepl:PRIMARY> rs.isMaster() 
    {
             "setName" : "myrepl",
             "ismaster" : true,
             "secondary" : false,
             "hosts" : [
                     "192.168.1.2:27017",
                     "192.168.1.3"
             ],
             "passives" : [
                     "192.168.1.4"
             ],
             "maxBsonObjectSize" : 16777216,
             "ok" : 1 
    }

  11. 測試 Replication 的功能。
    在 Primary 節點 (192.168.1.2) 新增資料
    myrepl:PRIMARY> use test; 
    switched to db test 
    myrepl:PRIMARY> db.test.games.insert({"title":"diablo III"}); 
    myrepl:PRIMARY> db.test.games.find() 
    { "_id" : ObjectId("4fcddcc83e54d133f505ee14"), "title" : "diablo III" }

    到 Secondary 節點查詢資料
    myrepl:SECONDARY> use test 
    switched to db test 
    myrepl:SECONDARY> rs.slaveOk() 
    not master and slaveok=false 
    myrepl:SECONDARY> db.test.games.find() 
    { "_id" : ObjectId("4fcddcc83e54d133f505ee14"), "title" : "diablo III" }

    請注意在預設的情況下,即使連結到 Secondary 節點也無法進行查詢的指令,必須透過 rs.slaveOk() 的指令才能宣告此一連線可用來進行查詢,此一設計的目的是為了避免程式誤連到 Secondary 後而不自知。因為 Secondary 節點與 Primary 節點內的資料存在一段的時間差,因此兩邊的資料可能不完全一致,這個現象對於某些應用來說將導致嚴重的不良後果,所以才需透過這種方式來確認程式的意圖。

  12. 測試自動切換的功能。
    我們故意將 Primary (192.168.1.2) 節點的 MongoDB 服務加以關閉,指令為
    service mongod stop

    此時我們在 192.168.1.3 這台主機執行 mongo 這個指定後,可以發現已經變成了 Primary。
    [root@mnode2 lib]# mongo 
    MongoDB shell version: 1.8.2 
    connecting to: test 
    myrepl:PRIMARY>
恭喜你,你已經設定好一個具備 HA 能力的 MongoDB Replica Set 了。最後,還有下列幾點事項需要提醒各位:

  1. 常用的相關指令,其中 db.printReplicationInfo() 可以用來查看 oplog 的使用狀況。 oplog 記錄 Secondary 節點同步時所必須執行的指令,如果檔案過小可能導致同步失效而必須執行完整同步 (resync)。 
    • rs.conf()
    • rs.reconfig()
    • rs.isMaster()
    • rs.status()
    • db.printReplicationInfo()
    • db.printSlaveReplicationInfo()
  2. Replica Set 在初始化時除了設定初始的節點外,都不可以含有資料 (除了 local 這個特殊的資料庫除外,這個資料庫永遠不會進行同步)。如果初始化時有節點已經包含資料,將會出現下列錯誤訊息:
    {
             "info2" : "no configuration explicitly specified -- making one",
             "errmsg" : "couldn't initiate : member 192.168.1.4 has data already, cannot initiate set.  All members except initiator must be empty.",
             "ok" : 0 
    }
  3. 前面提到 Primary 與 Secondary 之間存在時間差,所以資料並不會完全一致。因此對資料一致性有高需求程式,最好還是都從 Primary 節點進行資料的讀取。此外,還可以透過 write concern 來確保資料以被寫入”大部分”的節點。
  4. 節點還可以設定 hidden 屬性。Hidden 屬性為真的節點不會出現在 rs.isMaster() 的結果內,因此也不會被連結到 Replica Set 的驅動程式看到。這類 Hidden 節點可以專門作為備份、報表或分析之用。
  5. 雖然 MongoDB 至此已經具備自動切換的 HA 功能,但是程式本身如何連結 Replica Set 並處理失效的情況,仍是必須小心地加以設計。更重要的是,記得要做好足夠的測試才能確保不會在最不該發生意外的時候發生了令人遺憾的意外。 
  6. 前面我們透過 rs.reconfig() 這個指令將 192.168.1.4 這個節點重新設定為被動節點。除了可以改變現有節點的設定,rs.reconfig() 也可以用來動態地新增或刪除節點。不過在新增節點時要特別注意的是,新增後的節點會進行完整的同步動作。所以如果 Primary 節點已經存在大量的資料,同步動作可能會造成 Primary 節點過大的負載,因此建議還是選擇一個比較少人使用的時段再來進行比較恰當。
  7. 當發生 Primary 節點切換的動作時,原先的 Primary 節點與新的 Primary 節點之間如果存在任何資料的差異,那麼這些差異就會因此消失不見。舉例來說,如果我們新增一筆資料到原有的 Primary 節點,但是 Primary 節點卻還不及將這筆資料同步到其他節點前就失效了,那麼這筆新增的資料就不會被寫入其他節點。我們可以透過 write concern 來減少這類問題的發生。
  8. MongoDB 支援延遲同步的 Slave 節點,這個功能可以用來避免操作失誤 (例如誤刪資料) 所造成的不良後果。不過不管採用哪種 HA 方式,一個完整的備份計畫仍然是絕對必要的。HA 保持資料的即時性,而備份則可以確保資料的持久性。兩者的用途不盡相同,也各有運用的時機。
  9. 使用 Replica Set 後對整體系統效能的影響,是必須仔細地加以模擬並評估的。並不是所有的情況使用 Replica Set 後都可以獲得大量的效能改進。除了網路架構、硬體、系統架構、甚至是資料的讀寫比例等因素,都會對效能產生莫大的影響。同樣的,HA 也是必須審慎地加以模擬並評估。

2012年6月4日 星期一

[工具介紹] MongoDB Replica Set in CentOS 6.x (上)

logo-mongodb最近幾年,NoSQL 這個名詞很熱門,而在眾多 NoSQL 的解決方案當中,MongoDB 算是比較成熟且使用廣泛的一個資料庫 (data store)。在這篇文章中,我將說明一些有關 MongoDB HA (High Availability)/LB (Load Balancing) 的架構,並於下一篇文章實際建置一個可運作的 MongoDB Replica Set (MongoDB HA/LB 的一種方式)。

在開始建立測試環境之前,我們先來看一下 MongoDB 如何支援 HA/LB 等需求。MongoDB 共有三種方式可用來支援 LB,分別是 Master/Slave、Replica Set 與 Sharding,而其中 Master/Slave 與 Replica Set 更可以同時提供 HA 的能力。Replica Set 與 Master/Slave 架構類似,都是屬於 Replication 的方式,也就是同一筆資料會同時存放在多個資料庫節點當中。儘管有多個資料庫節點在同時運作,但是 Master/Slave 與 Replica Set 都僅支援單一 Master,也就是說在這許多的資料庫節點當中只有一個節點 (也就是 Master) 是可以被寫入的。而其他不是 Master 的節點,就僅能用來讀取資料。

如果要同時支援多重寫入的需求,目前僅能透過 Sharding 的方式來達成。不過 Sharding 跟常見的多重 Master 不同 (如 MySQL 的 MMM),並不是所有的資料庫節點都可以當做 Master,而寫入其中的資料會自動複製到其他節點。Sharding 是把一個 Collection 的資料,根據某鍵值 (這個特殊的鍵值又稱為 Shard Key) 的內容,將這個 Collection 內的資料分別儲存到不同的節點。因此一個單純的 Sharding 架構,是沒有具備 HA 的能力。當然,Sharding 可以 (也應該) 搭配 Replica Set 來達到 HA 的功能。

回到 Master/Slave 與 Replica Set 身上,兩者雖然都是 Replication 的架構,但是 Replica Set 支援自動切換 Master 的功能,所以能夠達到自動 HA 的需求。而 Master/Slave 的方式,一旦 Master 出現異常,必須經由手動的方式才能讓 Slave 變成 Master 並繼續提供相關服務。因此,除非是在很特殊的狀況下 (例如超過 Replica Set 所支援的節點上限),否則應該是盡量採用 Replica Set 的架構,而非 Master/Slave

在 Replica Set 的架構下,節點可分為三種形式,分別是

  1. 標準節點 (Standard)
    標準節點可以參與 Primary 的投票,也可以被選為 Primary,沒有被選為 Primary 的節點通稱為 Secondary。Primary 就是 Master,可以同時用來作為寫入與讀取之用,而 Secondary 僅能用來作為讀取之用。
  2. 被動節點 (Passive)
    被動節點可以參與 Primary 的投票,但是不能被選為 Primary 。被動節點也僅能用來作為讀取之後。
  3. 仲裁節點 (Arbiter)
    仲裁節點跟被動節點同樣可以參與 Primary 的投票,而且也不能被選為 Primary。跟被動節點不同的是,仲裁節點本身並不存放任何資料。

為什麼會有這麼多類型的節點?因為MongoDB 希望在因網路斷線而失效時,除了能夠避免兩個(或多個) Primary 所造成的不一致現象,同時還能夠確保最多的服務不受到影響。

舉例來說,如果某個 Replica Set 有三個標準節點+兩個被動節點,一旦 Primary 當機失效後,剩下的兩個標準節點與被動節點就會選出一個新的 Master。根據前面的節點類型說明,新的 Primary 將由剩下的兩個標準節點中選出,而選擇的依據則是根據節點的優先權與資料的更新程度。

env

primary down

但是如果今天發生的問題不是 Primary 失效,而是因為網路斷線而造成五個節點一分為二呢?如果原本的 Primary 剛好在擁有較多節點的那個區塊,則什麼事也不會發生 (除了另外一個區塊的節點無法更新資料)。但是如果原本的 Primary 所在的區段擁有較少的節點,那麼這個 Primary 就會自動降為 Secondary,而另外一個區段內的節點則會選出一個新的 Primary

network down

說完了 MongoDB Replica Set 的基本觀念,在下一篇文章當中我們就來實際建立一個可以運作的 Replica Set。

About