IPv6 Ready Logo Phase 2

最近受人之託, 處理了一下 IPv6 Ready Logo Phase 2 測試的 core 部份. 寫下一些經驗, 讓以後的人不用像我白繞很多冤枉路.

首先, 請慎選 TN 的 OS, FreeBSD 9.0-Release 以前的因為 ports 不支援, 碰到一堆怪問題也沒辦法透過 update 處理, 所以我從 9.0-Release 開始試, 一直試到 10.0-Release, 結論是: 請使用 FreeBSD 9.2-Release i386 當作你的 TN. 還有請不要企圖用 amd64 platform 的 FreeBSD 當 TN, 會遭受無法形容的痛苦.

接著, NUT 的部份, 如果是測 host, 請挑選一個「只有一個NIC」的設備來測試, 多個 NIC 的設備會在 SLAAC (Stateless Address Autoconfig) Phase 2 的部份遭受很多困境.

然後, 不要期待在 VM 裡頭隨便掛一個 Network Tag 就覺得會過, 請絕對要開另外一個專用的 Virtual Switch, 確保TN/NUT在同一個 vSphere Node 上.

最後, 請記得, 主流的 OS 基本上 IPv6 Stack 都是照著 RFC 設計的, 但是有些 OS 並不是 default apply RFC, 例如在 RFC 4862 的 NS/DAD 處理部份, BSD系列都要做過 sysctl 才能正確處理.

細節我就不貼上來, 畢竟對方也算是客戶, 需要什麼討論請私下.

「吃到飽」的無奈與罪惡

這個世界有很多「吃到飽」的服務, 餐廳, 3G網路, 音樂串流, 甚至是有線電視.

當然吃到飽是一個應需求而生的服務, 以餐廳為例, 一間餐廳若成為吃到飽的制度, 它為了經營成本的考量, 勢必會讓食物品質降低或者價金提高. 在食物品質可接受的範圍內自然有客戶群; 同樣的若需要更高品質食物的客戶群可以選擇非吃到飽的餐廳, 一切都處於一個收支平衡的狀態下.

對於3G網路, 則是一種不一樣的生態. 在基地台密度與頻寬有限的狀態下, 越多人選擇吃到飽的服務而且真的執行了他的權力, 無限制的去「用到飽」, 結局會是眾多使用者一起承擔連線品質下降的風險跟結局. 當然這責任並非在消費者身上, 而是在電信商是否執行他們的義務去隨著使用者用量擴增基地台與頻寬. 當然若是電信商在推出這樣方案時根本沒有仔細計算背後的成本結構, 一樣是電信商需要面對的失誤.

但是有些東西的「吃到飽」正在戕害市場.

舉一個最單純的例子: 當每個月149元可以聽幾乎所有的流行音樂聽到飽時, 消費者不會去購買CD, 因此在這個框架下, 認真的創作者跟不認真的創作者被打到同一條線上, 而且是一起去分食那僅存的每個月149元. 市場變小了, 認真的創作者難以生存之下只好濫竽充數求生存, 而不認真的創作者可以繼續進行他的不認真.

在供給面這扼殺了認真且有才華的創作者, 在需求面大眾被餵養了大量的低劣作品, 這個市場就會因此而崩壞.

當然對於消費者的反擊是, 許多流行音樂的專輯只有局部好聽或喜歡聽, 其餘的都屬於填版面的口水或芭樂歌, 自然消費者不願意花上整張專輯的錢去買兩三首歌, 而市場若已經有提供吃到飽服務的商家, 即使消費者擁有了購買自己喜歡的單曲的能力, 卻也會去選擇低價吃到飽的服務, 即使他們真的會去聽得僅限自己愛好的小圈圈.

這種狀況是短視的唱片公司, 無良的吃到飽商家, 還有被餵養的消費者共同創造出來的.

對於有線電視亦然, 一個月固定的價格給予了將近100台的選擇. 表面上是便宜又大碗, 但事實上各頻道商為了填塞每天至少6~8小時的節目, 又得應付越來越多的競爭者, 除了放棄高成本的自製之外, 還會將必須自製的節目(例如新聞台)的製作成本降到最低. 20年前的電視台新聞部可以負擔得起大量的記者去「跑」新聞, 而現在一個線只能配置一個「做」或「抄」新聞的記者, 這就是品質下降的鐵證. 更別說大量的小眾低品質電視台不但大多數觀眾根本不會去看, 還佔用了有限的頻寬, 於是台灣的電視台或頻道商還在做低品質, 低解析度節目餵養觀眾.

當然, 千錯萬錯都不會是消費者的錯, 消費者只是被動的選擇對自己最有利的方案, 或是在別無選擇的狀況下只能接受. 一個原本應該蓬勃的產業把自己搞到這種田地, 整個產業都該負起責任.

對於消費者又能夠做什麼呢?

我的想像是, 若消費者可以拒絕購買或使用這類「吃到飽」的低劣服務, 而改用其他方式付費支持獨立的發行商, 或者可以吸引唱片公司或是節目製作單位不要把自己精心創作的優質內容交給那些讓產業逐漸崩壞的商業模式, 而改用更為合理, 更把利潤回歸給創作者的管道去販售自己的創作物. 或許很不容易, 但是我想只要創作者跟消費者有共同的利益跟信念, 這樣的撥亂反正是有機會的.

很不容易, 我知道. 那我們技術人又能為這些崩壞的產業做些什麼呢? 靠技術去建立平台可能是個好方法吧.

至於那些頑冥不靈的大型沒落中的唱片公司或節目製作單位呢? 我想讓他們淹沒在時代的洪流裡, 是最後的慈悲.

Linux & Active Directory Integration Guide – Part.1: LDAP Auth

前言

這篇指南主要討論的是, 以一般辦公室環境, 由於大多數用戶端皆為Windows, 為達到統一管理及單一登入(Single Sign On)的需求, 一般會使用Windows Active Directory網域來達成, 但若其餘系統例如Mail Server, 內部ERP等等均使用Microsoft產品或相容Microsoft之產品, 不僅在授權費用上昂貴, 相對於Open Source解決方案而言也較無彈性.

為求管理便利與成本控管, 較好的方式是內部使用者皆以Active Directory管理, 但其餘相關系統均透過Active Directory提供的LDAP或是採用Samba提供的winbind服務做認證整合. 除可同時達到前述兩項好處之外, 又可提供內部MIS人員在操作帳號及權限時的統一界面, 較低的學習曲線有助於人力預算的控制及減少錯誤與漏失的發生率.

此篇便是以此為基礎, 架設一套Active Directory網域後, 透過LDAP方式整合. 下一篇會敘述如何以Samba的winbind服務將非Windows環境的CIFS/NFS Sharing整合進Active Directory的群組原則(Group Policy)與目錄權限等控制.

以下先行敘述測試環境的相關資料.

Active Directory部份

  • OS: Windows Server 2008R2 Ent. with SP-1/Latest Update (截至Oct 2012為止)
  • FQDN: pdc.lab.com
  • IP: 192.168.9.9

Linux部份

  • OS: CentOS 6.3 x64
  • FQDN: linux.lab.com
  • IP: 192.168.9.7

準備Windows Active Directory環境

  1. 安裝Windows Server 2008R2 Ent, 並且完成最新的Windows Update
  2. 使用 dcpromo.exe 建立新樹系 (範例中為lab.com)
  1. 安裝 Microsoft Identity Management for UNIX, 包含預設的 “Password Synchronization” 與 “Server For NIS”
  1. 建立專用於Unix的使用者群組, 並設定好NIS Domain與GID. 此處UID/GID均從10000作為起始
  1. 取消密碼複雜性需求設定, 以便後續測試 (Optional)
  1. 建立一使用於LDAP bind的帳號, 此處使用帳號為 ldapbind, 密碼為 secretone 並將此帳號併入 Domain Guests 群組.
  1. 建立一般使用者帳號, 除正常Domain Users之外, 將其歸入Unix Users群組, 並設定其 NIS Domain, GID, UID 等.

到此大致完成Windows端的設定.


準備Linux端設定

  1. 安裝 CentOS 6.3 x64, 此處以最小安裝即可
  2. 關閉 selinux, 開啟remote ssh root login, 關閉 iptables, 並重開機使其生效 (Optional)


# sed -i 's/=enforcing/=disabled/g' /etc/sysconfig/selinux
# sed -i 's/=enforcing/=disabled/g' /etc/selinux/config
# sed -i 's/#PermitRootLogin/PermitRootLogin/g' /etc/ssh/sshd_config
# /sbin/chkconfig iptables off
# /sbin/chkconfig ip6tables off
# /sbin/chkconfig sshd on
# reboot

  1. 安裝 rpmforge 與 EPEL repo, 並將套件更新至最新版


# rpm -Uvh http://packages.sw.be/rpmforge-release/rpmforge-release-0.5.2-2.el6.rf.x86_64.rpm
# rpm -Uvh http://mirror01.idc.hinet.net/EPEL/6/i386/epel-release-6-7.noarch.rpm
# yum -y update

  1. 安裝必須套件


# yum -y install perl pam_krb5 vim-enhanced nss-pam-ldapd ntpdate crontabs

註: 這裡特別裝了 vim-enhanced 是因為一個簡單的理由: 我慣用! 其實跟一切都沒太大關係的 XD.

  1. 設定/etc/hosts, 裡頭至少要有自己跟PDC的 FQDN


127.0.0.1 localhost localhost.lab.com localhost4 localhost4.lab.com4
::1 localhost localhost.lab.com localhost6 localhost6.lab.com6
192.168.9.9 pdc pdc.lab.com
192.168.9.7 linux linux.lab.com

  1. 設定時間同步, 這對Active Directory很重要. AD不允許PDC跟client間有超過300秒的時間差


# crontab -e

內容為

0 * * * * /usr/sbin/ntpdate pdc.lab.com 2>&1 >/dev/null

  1. 編輯 /etc/krb5.conf, 設定好 Kerberos 5 的認證

內容如下, 請注意裡頭該大寫的地方就真的得放大寫.

[logging]
default = FILE:/var/log/krb5libs.log
kdc = FILE:/var/log/krb5kdc.log
admin_server = FILE:/var/log/kadmind.log

[libdefaults]
default_realm = LAB.COM
dns_lookup_realm = true
dns_lookup_kdc = true
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true

[realms]
LAB.COM = {
kdc = pdc.lab.com:88
kdc = pdc.lab.com
admin_server = pdc.lab.com:749
default_domain = lab.com
kdc = pdc.lab.com
}

[domain_realm]
.lab.com = LAB.COM
lab.com = LAB.COM

[kdc]
profile = /var/kerberos/krb5kdc/kdc.conf

[appdefaults]
pam = {
debug = false
ticket_lifetime = 36000
renew_lifetime = 36000
forwardable = true
krb4_convert = false
}

  1. 編輯 /etc/nslcd.conf 與啟動 nslcd 服務

內容如下

uri ldap://pdc.lab.com/
binddn cn=ldapbind,cn=Users,dc=lab,dc=com
bindpw secretone
scope sub
base cn=users,dc=lab,dc=com
scope group sub
scope hosts sub
pagesize 1000
referrals off

filter passwd (&(objectClass=user)(!(objectClass=computer))(uidNumber=*)(unixHomeDirectory=*))
map passwd homeDirectory unixHomeDirectory
map passwd gecos displayName

filter shadow (&(objectClass=user)(!(objectClass=computer))(uidNumber=*)(unixHomeDirectory=*))
map shadow uid sAMAccountName
map shadow shadowLastChange pwdLastSet

filter group (objectClass=group)
map group uniqueMember member

uid nslcd
gid ldap
ssl no
tls_cacertdir /etc/openldap/cacerts

編好存檔後, 啟動 nslcd

# /sbin/chkconfig nslcd on
# /sbin/service nslcd start

  1. 編輯 /etc/nsswitch.conf

在裡頭 passwd, shadow, group 的部份都加上 ldap, 大致如下

passwd: files ldap
shadow: files ldap
group: files ldap

  1. 編輯 /etc/openldap/ldap.conf

內容如下

URL ldap://pdc.lab.com/
BASE dc=lab,dc=com
TLS_CACERTDIR /etc/openldap/cacerts

  1. 測試是否連結完成

可使用 getent passwd 跟 getent group 確認LDAP群組是否已經掛上去了

# getent passwd
… 上略 …
nslcd:x:65:55:LDAP Client User:/:/sbin/nologin
ntp:x:38:38::/etc/ntp:/sbin/nologin
nekobe.wu:*:10000:10000:nekobe.wu:/home/nekobe.wu:/bin/bash

# getent group
… 上略 …
nscd:x:28:
ldap:x:55:
ntp:x:38:
Unix Users:*:10000:nekobe.wu

只要在最後看到有個來自於AD建立的群組跟使用者, 大致上就是成功了


設定系統認證

  1. 編輯 /etc/pam.d/system-auth

內容如下

#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth required pam_env.so
auth sufficient pam_unix.so nullok try_first_pass
auth requisite pam_succeed_if.so uid >= 500 quiet
auth sufficient pam_krb5.so use_first_pass
auth sufficient pam_ldap.so use_first_pass
auth required pam_deny.so

account required pam_unix.so broken_shadow
account sufficient pam_localuser.so
account sufficient pam_succeed_if.so uid < 500 quiet
account [default=bad success=ok user_unknown=ignore] pam_ldap.so
account [default=bad success=ok user_unknown=ignore] pam_krb5.so
account required pam_permit.so

password requisite pam_cracklib.so try_first_pass retry=3 type=
password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok
password sufficient pam_krb5.so use_authtok
password sufficient pam_ldap.so use_authtok
password required pam_deny.so

session optional pam_keyinit.so revoke
session required pam_limits.so
session optional pam_mkhomedir.so
session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session required pam_unix.so
session optional pam_krb5.so
session optional pam_ldap.so

其中 pam_mkhomedir.so 是為了讓沒有home dir 的使用者依然可以登入, 並且於登入後自動建立, 免除管理人員還要進Server建目錄的困擾.

  1. 使用authconfig更新pam設定


# authconfig –updateall

  1. 測試ssh是否可登入


XShell:\> ssh nekobe.wu@192.168.9.7

Connecting to 192.168.9.7:22…
Connection established.
Escape character is ‘^@]’.

Creating directory ‘/home/nekobe.wu’.
Last login: Thu Oct 25 14:37:22 2012 from localhost
[nekobe.wu@linux ~]$
如此一來就算大功告成, 後續其他Linux相關認證都可以這樣比照辦理.


其他需要知道的

  1. 如果不想讓 AD 上的 user 登入到 Unix 去, 除了可以在/etc/nslcd.conf 裡頭變更 passwd 與 group 的 filter 之外 (例如限制 memberOf 的方式), 也可以在AD Attribute裡頭把 loginShell 改成 /bin/false, 如此一來可以讓User認證其他服務如 Email, FTP等等, 但是卻無法sshd/telnet 登入.
  2. 若是沒有在AD端設定好Unix相關attributes, 例如uidNumber, gidNumber, unixHomeDirectory, loginShell 等等重要屬性, 會導致因 filter 設定的關係無法登入, debug時要注意這一點, 也或者可以拿這個condiction當成過濾的方式.
  3. 若是nslcd一直無法設定完成, 可以先停掉服務, 改用 debug mode


# /sbin/service nslcd stop
# nslcd -d
nslcd: DEBUG: add_uri(ldap://pdc.lab.com/)
nslcd: DEBUG: ldap_set_option(LDAP_OPT_X_TLS_CACERTDIR,"/etc/openldap/cacerts")
nslcd: version 0.7.5 starting
nslcd: DEBUG: unlink() of /var/run/nslcd/socket failed (ignored): No such file or directory
nslcd: DEBUG: setgroups(0,NULL) done
nslcd: DEBUG: setgid(55) done
nslcd: DEBUG: setuid(65) done
nslcd: accepting connections


以上, 預計還會再做的部份是 Samba 的 winbind 整合目錄與群組權限管理, 以及更精細的從AD端控制Unix端使用, 還有各種如 email, ftp 等相關服務.

笨蛋!關鍵是專業度!

這篇的由來也是因為前幾天發生了一段對話, 讓我想到過去發生(在別人身上 :p)過的慘痛經驗, 就想寫成一篇來分享一下. 畢竟我不是像 Joe Chang & Bryan Yao 這麼專業的顧問, 也常自哂是個沒讀什麼書的粗人, 沒辦法很系統化的去描述大框架的概念, 也只能想到什麼寫什麼了. (這100%是硬要牽拖的友情廣告贊助, Joe/Bryan請留一支黑牌給我當廣告費)

事情是, 某公司用的MySQL Database大概是因為Loading太高, 常常出現一些靈異現象: 當機也就罷了, 常常重啟之後這個Table壞那個Table少資料, 更可怕的是有時候Table會消失. 每次碰到這種麻煩, MIS/網管只好能修則修, 不能修只好倒備份回去. 可是每次倒備份都會有一段時間的資料消失, 甚至是好幾個DB之間相互參照的部份會有資料同步問題, 所以只好讓Programmer去寫一些檢查資料同步與完整性的工具, 然後讓備份更頻繁, 去降低發生時的損傷.

剛開始也只是電話裡口頭給一些可能的解決方案跟方向, 也請他們做過分析最佳化了Schema, 但是也只是稍微改善, 並沒有完全排除這個問題. 直到他們老闆終於首肯讓我進他們系統看看時, 已經是個AQPS(avg queries per second)超過20k的超可怕DB Host了, 當下我只好建議他們往Cluster方向走, 然後有了之前那堆MySQL架構的step by step. (話說到這, Jason你欠我的大餐哩? XD)

當下他們也就從善如流花了一些錢買新設備, 做好了Cluster, 但是顧及線上服務還在走, 他們有計劃的一點一點把東西從舊的搬到新的Cluster去, 悲慘的事情於焉發生, 這時他們備份系統用的磁帶櫃掛了, 暫時沒辦法維持原本的double daily backup, 只能weekly, 而他們心想, 反正已經搬走了30%的loading, 應該不會這麼衰在修好前又掛了吧! 所以就說莫非定律一定在這種莫非的時候發生, 真的突然掛了.

他們雖然也就照以前的災難復原SOP去做, 但是這次備份的間隔有點大, 實在會被罵的太用力, 而他們的DBA也很坦白的說無能為力, 可能得去跟Oracle買MySQL的Consult Service看他們能不能從壞掉的Datafile裡頭救, 於是他們還是找上我.

當我Remote連上去, 看到那些壞的徹底的datafile也是感到很絕望, 但是卻同時看到希望: 他們還有著整整兩個月份量的Binary Log都沒Purge掉, 這真是太神奇了傑克! 所以就確認他們備份的時間, 把那個時間後的Binary Log用mybinlog工具倒出來, 再一股腦的倒回去, 系統也就順利的回到掛掉的瞬間.

這個故事要說的是, 當你覺得你很熟悉一個系統或工具或語言時, 可能還遠遠不夠, 而決定你是「物有所值」或是「物超所值」的關鍵是專業度.

我的老婆是老大(My Wife Is a Gangster)片段

韓國笑片我的老婆是老大(My Wife Is a Gangster)裡頭有個爆笑對話但是正好切題.

流氓A: 你知道小混混跟流氓的差別是什麼?
流氓B: ….(疑惑)
流氓A: 笨蛋, 是專業度!

剛剛那個案例裡, 那家公司並非草包, 技術人員們也各各身懷絕技, 但是他們敗在以為自己太熟悉系統而忘記永遠要知其所以然, 所以縱然有這麼好的身手跟技術, 卻不知道其實早就有Binary Log及解開Binary Log的工具可以幫助我們做Data Restore. 如果他們能相信自己不夠專業, 而認真的閱讀手冊, 看mailing list, 而不是自滿於管理過20k AQPS的大型(其實這種規模大概只能算中型)DBMS而覺得自己已經夠專業, 那麼他們不但可以更快更安穩的解決他們碰到的問題, 更可以提高自己在老闆眼中的身價.

我們也常常看到履歷表中, 求職者洋洋灑灑寫了一堆自己會的東西, 甚至會有人寫精通, 但是我卻總是很迷惑於「究竟是多精通?」, 每次Interview後又總是失望, 可能只是下載過幾個Linux Distro然後照著鳥哥的文件一步一步弄了一些常用的service, 就寫上精通. 或者是只是唸書上計概學過一些VB寫過「Mini-Calc」程式作業然後就寫上精通. 這又是另外一種不夠專業的典型.

身為一個技術人員, 專業是必須的, 一樣當你覺得自己夠專業了, 其實就斷絕了自己邁向專業的可能.

喔對了, 當流氓也是一樣的! XD

投資現在, 還是投資未來?

先說一個真實故事, 這個故事我甚至不用解釋或強調真實性, 因為同樣的pattern在這個世界到處在發生.

某A公司是一個新創的IC Design House, 創辦人Peter是個在這方面學有專精又有想法的經營者, 他在辛苦經營了三年終於開始轉虧為盈, 第四年更進一步成長到50人規模的Design House, 同時也開始賺進了第一桶金: 純利1200萬.

員工們都很開心, 辛苦了這些年終於開花結果, 大家都在期待著手上擁有專利與穩定客戶的公司何時會開始增資, 何時開始準備上市上櫃, 當然還有, 這一年的年終獎金會是怎樣呢?

這時候Peter在尾牙上告訴大家, 這1200萬的純利, 他會拿出其中200萬當作額外的獎金, 當台下員工議論紛紛的同時, 他接著說, 他要將其於的1000萬投資在公司的開發, 測試, 及模擬系統上.

當時整間公司嘩然, 一個僅50人, 規模只能堪稱剛起步的公司, 不但短時間內用不到那上億的專業系統, 更不用說一千萬幾乎什麼都買不到. 所以私下討論是不是Peter要中飽私囊, 甚至開始有同事正在觀望年後是不是要跳槽到其他公司去.

第五年, Peter並沒有買進大批系統需要的設備軟硬體, 也沒有在財務報表上提示出這一千萬作為股東配息, 反而是把這一千萬直接現金增資, 並且成立了一個直屬於他的系統團隊. 他高薪挖來了一個曾在德國某S公司擔任過CIO的老外, 並且讓這個老外組織了一個平均年齡僅25歲的Programmer團隊. 這一年公司的業績大幅成長, 原先編制也上漲到接近100人, 而第五年底的財務狀況是純利2800萬 — 不算這個系統團隊的話, 算進這個系統團隊的成本之後, 只獲利了1500萬, 而這一年這個系統團隊沒有任何產出, 所以開始有些人離職了. 而Peter也同時把1500萬的純利抽出1200萬加碼投資這個系統團隊.

第六年時因為流失了一些關鍵人才, 所以人數及營收都沒有大幅成長.

第七年發生了一件事, 國外某H大廠為了他們標下了美國國防部的標案, 下了一筆極大量, 對產品品質及彈性都十分要求的大單給A公司的對手R公司, 而R公司不但跟A公司幾乎同時間成立, 產品線跟客戶群也和A公司相近, 最大的不同是R公司在第五年才損益平衡, 但是第七年時已經是比A公司大兩倍, 準備上櫃的明星公司.

悲劇來了, 由於R公司並沒有建立足夠專業及自動化的開發測試模擬系統上, 因此第一批出貨就慘遭退貨. 這時Peter趕到國外去見H大廠的高層, 然後帶著一疊文件回來.

他在關著房門跟那個神秘又燒錢的系統團隊密集開了一個禮拜的會之後, 員工們看見一車一車的硬體設備被送進公司, 系統團隊的同事們忙近忙出. 兩週後, Peter招開了一個員工大會.

他在員工大會上花了2個小時跟所有工程師解釋系統團隊三年來的成就 — 一套嶄新, 符合他們工作流程, 自行開發的模擬系統, 並且要求所有工程師要在半個月內熟悉這套系統, 因為他們要承接H大廠的超級大單了.

後頭的故事就不用我說了, A公司已經是個數千人的上市公司, 而這位國外高薪聘請回來的CIO, 說服了董事會由A公司100%轉投資另外一家軟體開發公司, 他們的模擬軟體在兩年內拿下那個專有市場的30% marketing share. 業內同質公司業績越好, 越需要他的軟體, A公司就越賺. 同時A公司可以依靠比別家公司更省的成本, 得到更多的大單. 而R公司不但成為他們轉投資軟體公司的客戶, 過了幾年也被A公司併購了. 也就是說Peter用了三年共約四千萬的投入, 省下了接近六千萬的軟體, 而且這是在他剛看到黎明曙光的第四年下的決定 — 盡管他可能在當下都不能肯定他未來需不需要這個系統.

說這個故事, 其實重點是談幾件事情.

  1. 當所有人投資現在, 他們會是現在的贏家. 但能投資未來的人才會是未來的贏家.
  2. 在天還沒亮之前, 任何長遠的投資都像買樂透一般 — 因為你有極大的機率付不出second round的投資, 而你的first round, 就成為捨不得也沒辦法的沉默成本.
  3. 信任你的領導人, 尤其是他曾經帶領著大家打過勝仗
  4. 永遠不要忘記投資自己, 因為那將是最好的回收

其實額外還想藉著這個主題談一件好像不相關的事, 但是我卻想在這選前之夜如以往一樣的呼籲….

那就是, 去投票吧! 投票, 就是對國家未來最好的投資.

在成為SA之前….

前幾天, 一個案子上認識的客戶端Programmer透過msn問了我一個問題, 讓我想了很久, 思考之餘才生出了這篇文章.

記得我在某篇文章說過, 技術顧問不是一種身份, 而是一種狀態: 因為你在技術層面擁有了足夠的深度及廣度, 所以你成為一個可以當技術顧問的人. 事實上, SA(System Analyst)也是.

我常會跟同事或同行的朋友強調, 資訊科技中沒有什麼叫做定律, 這畢竟還是個非常年輕的領域, 一切都還在發展與進步中, 每一個曾被認為是圭臬的教條, 很可能很快的在新技術發生後, 變成不值一哂廢話. 這告訴我們, 不要被知識迷惑, 學習那些知識跟技能最重要的知道為什麼, 而不是把怎麼做變成SOP般的教條.

在Wikipedia裡頭有關於System Analyst的敘述, 裡頭列出了如下的幾個SA的工作內容:

  • Plan a system flow from the ground up.
  • Interact with customers to learn and document requirements that are then used to produce business requirements documents.
  • Write technical requirements from a critical phase.
  • Interact with designers to understand software limitations.
  • Help programmers during system development, ex: provide use cases, flowcharts or even Database design.
  • Perform system testing.
  • Deploy the completed system.
  • Document requirements or contribute to user manuals.
  • Whenever a development process is conducted, the system analyst is responsible for designing components and providing that information to the developer.

喔, 有夠複雜的, 簡單的說, 一個SA作的就是, 從需求到藍圖之間, 從藍圖到專案完成之間, 幾乎所有的工作.

在很多大型的專案會拆的更細, 例如會有SD(System Designer)負責系統架構的設計, 會有UD(UI Designer)負責統整設計符合需求流程又同時易於使用的UI, 有DBA(Database Administrator)負責去規劃後端資料庫schema….等等. 但是其實這些都只是把SA從一個人變成一個小組, 實際在主導這些工作的, 還是一個Major/Chief SA在負責.

所以, 要成為一個SA, 門檻相當相當的高, 除了要熟悉「各種」系統, 更要熟悉「各種」語言, 熟悉「各種」資料庫, 甚至要會使用者行為分析, UI設計原則等等. 這還只是基本要求, 更深入的說SA還要具備良好且有效率的溝通能力, 技術判斷力(例如判斷什麼需求必須被擱置或拋棄, 什麼沒提出的需求必須被隱性的加入規格中), 管理能力….etc.

很多Programmer都把自己的下一步定義在SA, 但是往往缺乏對SA這個身份的認知. 某D是我遇過的Web Game Programmer, 他的目標是成為SA, 但是當我鼓勵他多接觸系統面的東西時, 他告訴我他只想寫Game, 其他部份他認為沒必要學; 某J是Java Programmer, 他的目標也是成為SA, 但是當我鼓勵他多接觸其他語言, 多理解一些各語言的特性與優缺點時, 他告訴我他覺得Java最好, Java是萬能的, 不須要學其它語言.

某D還在繼續寫Web Game, 用PHP寫了五年的Web Service, 但是他依然不懂REST是什麼; 某J換了幾家公司, 還在持續寫Java, 卻總搞不清楚他的程式為什麼效能總是不好. 但是他們的共通點是, 永遠都在抱怨主管不賞識, 不加薪, 不給機會讓他表現.

我的職涯之中, 做過各種身份, 在這個業界十多年的經驗與累積, 直到目前為止我都還認為我自己距離一個60分的SA至少還差一大截需要努力, 我實在想問問這些死守在一個領域, 單一平台, 單一語言的年輕後輩們, 你憑什麼覺得自己可以是個SA?

好吧, 那SA這種高度要求的艱難身份, 究竟該如何達到?

我能給予的建議是, 忘記自己熱愛的語言–去學更多語言, 忘記自己習慣的系統–去用更多種系統, 忘記自己對專業的堅持–去跟User做更深入的溝通, 持續的做, 然後當你不再為了怎麼成為一個SA而迷惑時, 你就可以是個SA了.

回到最開始的問題, 他問的是:「我想去IBM應徵SA, 你覺得有機會嗎?」

偉哉, 他只寫了三年ASP, 連個Paper Engineer必備的MCSE課程都沒上過啊….

MySQL Cluster Installation Guide

2012/01/10更新: Startup Scripts在最底下

照規矩, 這是一個 Run Down, 有問題請看文件.

MySQL Cluster是一個HA架構(High Availability)的MySQL服務, 當單一MySQL不夠用, 換成 Master-Slave Replication 也不夠用, 換到MySQL Proxy 時也出現了單一Master的寫入瓶頸時, MySQL Cluster 是不用變更目前程式架構下, 最後一個Solution.

MySQL Cluster有三種身份: Management Node, Data Node, SQL Node三種, Management Node是Cluster的judgement role, 負責控制各個Node之間的狀態, 在某個Node出現問題時自動切換, 或是重要的Node出狀況時會停掉服務避免資料遺失; Data Node 是實際存資料的Node, 原始結構依然是類似Master-Slave的Replication關係; SQL Node是提供標準mysql protocol 的前端frontend, 拿來提供服務用的.

如果只是為了提供很容易橫向擴充的結構, 那在資料保全的前提下, 基本消費是四台: Management Node一台, Data Node兩台, SQL Node一台. 如果是為了達到完整的HA, 也就是避免任何一種OPF(One Point Failure), 那需要的基本消費就五台:Management Node一台, Data Node兩台, SQL Node兩台, 配合OS本身做的NIC bond, 可以做到軟硬體各層的OPF-Free HA. 配合L4 Switch或是Haproxy一類的L4 Base LBS, 跟TCP Healthy Check, 可以做到無限量橫向擴充. 大致上的架構如下圖.

MySQL Cluster

以下是簡單的Run Down, 範例是使用CentOS 6.0 x64, 共需要五台, 以下是範例用的IP配置:

  • Management Node (ndb_mgmd): 192.168.0.1 [mgmt]
  • Data Node (ndbd Master): 192.168.0.2 [master]
  • Data Node (ndbd Slave): 192.168.0.3 [slave]
  • SQL Node (mysqld A): 192.168.0.4 [front-a]
  • SQL Node (mysqld B): 192.168.0.5 [front-b]

以下是五台共通的部份.

  1. 安裝CentOS 6.0, 只裝Minimum的Base即可.
  2. 安裝完成後, 關閉 ip6tables, iptables, postfix

    # /sbin/service ip6tables off
    # /sbin/service iptables off
    # /sbin/service postfix off

    同時編輯 /etc/selinux/config 把 selinux關掉, 把enforcing 改成 disabled

    # This file controls the state of SELinux on the system.
    # SELINUX= can take one of these three values:
    # enforcing – SELinux security policy is enforced.
    # permissive – SELinux prints warnings instead of enforcing.
    # disabled – No SELinux policy is loaded.
    SELINUX=disabled
    # SELINUXTYPE= can take one of these two values:
    # targeted – Targeted processes are protected,
    # mls – Multi Level Security protection.
    SELINUXTYPE=targeted

    關掉iptables跟selinux的原因是避免安裝與設定時碰到麻煩事, 請記得在裝好之後, 確認一切沒問題了, 重新打開並且設定好 firewall ruleset

  3. 把 postfix 跟 mysql-libs 移除, 因為mysql-libs跟MySQL Cluster衝突, 而postfix又得dependence mysql-libs.

    # yum -y erase postfix mysql-libs

    接著做一次 yum update 升級一下, 然後reboot讓剛剛修改selinux的部份跟關掉service的部份生效

    # yum -y update
    # reboot

接下來是各台不同的部份. 先從Mgmt Node開始

  1. 去抓取以下兩個檔案, 請依照你裝的CentOS 版本抓取適合的 x86_64 或 i386 版本.
    • MySQL-Cluster-gpl-management-7.2.0_devmilestone-1.rhel5 [x86_64] 或 [i386]
    • MySQL-Cluster-gpl-tools-7.2.0_devmilestone-1.rhel5 [x86_64] 或 [i386]

    抓好之後, 用以下指令安裝

    # rpm -Uhv MySQL-Cluster-gpl-management-7.2.0_devmilestone-1.rhel5.x86_64.rpm
    # rpm -Uhv MySQL-Cluster-gpl-tools-7.2.0_devmilestone-1.rhel5.x86_64.rpm

然後是 Data Node

  1. 抓取以下檔案
    MySQL-Cluster-gpl-storage-7.2.0_devmilestone-1.rhel5 [x86_64] 或 [i386]
    然後安裝

    # rpm -Uhv MySQL-Cluster-7.2/MySQL-Cluster-gpl-storage-7.2.0_devmilestone-1.rhel5.x86_64.rpm

最後是 SQL Node

  1. 抓取以下檔案
    • MySQL-Cluster-gpl-server-7.2.0_devmilestone-1.rhel5 [x86_64] 或 [i386]
    • MySQL-Cluster-gpl-client-7.2.0_devmilestone-1.rhel5 [x86_64] 或 [i386]

    然後安裝

    # rpm -Uhv MySQL-Cluster-gpl-server-7.2.0_devmilestone-1.rhel5.x86_64.rpm
    # rpm -Uhv MySQL-Cluster-gpl-client-7.2.0_devmilestone-1.rhel5.x86_64.rpm

裝完之後, 開始設定與啟動.

  1. 先到 Management Node 上建立設定檔目錄跟設定檔

    # mkdir -p /var/lib/mysql-cluster
    # vi /var/lib/mysql-cluster/config.ini

    其中內容如下

    [ndbd default]
    NoOfReplicas=2
    DataMemory=80M
    IndexMemory=18M
    [tcp default]
    portnumber=2202
    [ndb_mgmd]
    hostname=192.168.0.1
    datadir=/var/lib/mysql-cluster
    [ndbd]
    hostname=192.168.0.2
    datadir=/var/lib/mysql-cluster
    [ndbd]
    hostname=192.168.0.3
    datadir=/var/lib/mysql-cluster
    [mysqld]
    hostname=192.168.0.4
    [mysqld]
    hostname=192.168.0.5
  2. 接著到Data Node跟SQL Node上建立 /etc/my.cnf

    # vi /etc/my.cnf

    內容如下

    [mysqld]
    ndbcluster
    ndb-connectstring=192.168.0.1

    [ndbd]
    connect-string=192.168.0.1

    [ndb_mgm]
    connect-string=192.168.0.1

    [ndb_mgmd]
    config-file=/var/lib/mysql-cluster/config.ini

  3. 都設定好之後, 先到 Management Node上啟動ndb_mgmd

    # ndb_mgmd -f /var/lib/mysql-cluster/config.ini –initial
    MySQL Cluster Management Server mysql-5.1.51 ndb-7.2.0-beta
    2011-07-25 23:20:02 [MgmtSrvr] INFO — The default config directory ‘/usr/mysql-cluster’ does not exist. Trying to create it…
    2011-07-25 23:20:02 [MgmtSrvr] INFO — Sucessfully created config directory
  4. 接著到Data Node啟動ndbd (兩台都要)

    # ndbd
    2011-07-25 23:21:13 [ndbd] INFO — Angel connected to ‘192.168.0.2:1186’
    2011-07-25 23:21:13 [ndbd] INFO — Angel allocated nodeid: 3
  5. 最後啟動SQL Node (一樣兩台都要)

    # /sbin/service mysql start
    Starting MySQL. SUCCESS!
  6. 回到 Management Node 上, 用以下指令檢查狀況

    # ndb_mgm -e show
    Connected to Management Server at: localhost:1186
    Cluster Configuration
    ———————
    [ndbd(NDB)] 2 node(s)
    id=2 @192.168.0.2 (mysql-5.1.51 ndb-7.2.0, Nodegroup: 0, Master)
    id=3 @192.168.0.3 (mysql-5.1.51 ndb-7.2.0, Nodegroup: 0)

    [ndb_mgmd(MGM)] 1 node(s)
    id=1 @192.168.0.1 (mysql-5.1.51 ndb-7.2.0)

    [mysqld(API)] 2 node(s)
    id=4 @192.168.0.4 (mysql-5.1.51 ndb-7.2.0)
    id=5 @192.168.0.5 (mysql-5.1.51 ndb-7.2.0)

    這樣就表示成功了.

  7. 接著就可以到SQL Node上用MySQL Client Command Line去連線並且開始執行SQL指令

    # mysql -u root
    Welcome to the MySQL monitor. Commands end with ; or \g.
    Your MySQL connection id is 3
    Server version: 5.1.51-ndb-7.2.0-devmilestone-cluster-gpl MySQL Cluster Server (GPL)

    Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved.
    This software comes with ABSOLUTELY NO WARRANTY. This is free software,
    and you are welcome to modify and redistribute it under the GPL v2 license

    Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.

    mysql>

    看到上頭版本是 5.1.51-ndb-7.2.0-devmilestone-cluster-gpl MySQL Cluster Server (GPL)就大功告成啦, 可以開始試著執行指令並看看有沒有自動被同步到另外一台SQL Node上.

  8. 要關閉MySQL Cluster時請反過來執行, 先到兩台SQL Node上關掉mysqld

    # /sbin/service mysql stop

    再到Management Node關閉

    # ndb_mgm -e shutdown

    這個指令會同時關掉 Data Node 的 ndbd 及Management Node上的ndb_mgmd

以上就是簡單的Run Down, 如果要架設前端的Layer 4 Switch, 選用TCP Ping當作Healthy Check Algorithm即可.
大功告成, 有問題請洽Google大神.

2012/01/10更新
ndb_mgmd 的 startup scripts內容如下

#!/bin/sh
# Source function library.
. /etc/rc.d/init.d/functions

start()
{
echo -n $”Starting ndb_mgmd: ”
daemon /usr/sbin/ndb_mgmd -f /var/lib/mysql-cluster/config.ini

touch /var/lock/subsys/ndb_mgmd
echo
}

stop()
{
echo -n $”Shutting down ndb_mgmd: ”
killproc ndb_mgmd

rm -f /var/lock/subsys/ndb_mgmd
echo
}

[ -f /usr/sbin/ndb_mgmd ] || exit 0

# See how we were called.
case “$1″ in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
;;
condrestart)
[ -e /var/lock/subsys/ndb_mgmd ] && (stop; start)
;;
*)
echo $”Usage: $0 {start|stop|restart|reload|condrestart}”
exit 1
esac

exit 0

新建一個 /etc/init.d/ndb_mgmd之後

# chmod a+X /etc/init.d/ndb_mgmd
# /sbin/chkconfig –add ndb_mgmd
# /sbin/chkconfig ndb_mgmd on
# /sbin/service ndb_mgmd start

ndbd (Data Store Node) 的Startup Scripts如下

#!/bin/sh
# Source function library.
. /etc/rc.d/init.d/functions

start()
{
echo -n $”Starting ndbd: ”
daemon /usr/sbin/ndbd

touch /var/lock/subsys/ndbd
echo
}

stop()
{
echo -n $”Shutting down ndbd: ”
killproc ndbd

rm -f /var/lock/subsys/ndbd
echo
}

[ -f /usr/sbin/ndbd ] || exit 0

# See how we were called.
case “$1″ in
start)
start
;;
stop)
stop
;;
restart|reload)
stop
start
;;
condrestart)
[ -e /var/lock/subsys/ndbd ] && (stop; start)
;;
*)
echo $”Usage: $0 {start|stop|restart|reload|condrestart}”
exit 1
esac

exit 0

一樣新建在 /etc/init.d/ndbd

# chmod a+x /etc/init.d/ndbd
# /sbin/chkconfig –add ndbd
# /sbin/chkconfig ndbd on
# /sbin/service ndbd start

MySQL Proxy for CentOS Configuration Guide

Update on 2011/07/17
如果碰到

2011-07-17 02:56:25: (critical) proxy-plugin.c:263: read_query_result() in /etc/mysql-proxy/proxy.lua tries to modify the resultset, but hasn’t asked to buffer it in proxy.query:append(…, { resultset_is_needed = true }). We ignore the change to the result-set.

這樣的問題的話, 修改 /etc/mysql-proxy/proxy.lua 的第 201 行, 把

proxy.queries:append(1, packet)

改成

proxy.queries:append(1, packet, {resultset_is_needed = true})

就可以了

一樣是個Run down, 中間碰到問題請找Google大神
基本準備三台機器, 兩台互為MySQL Server Master/Slave, 請參考MySQL Replication (Master-Slave) for CentOS Installation Guide(後文各稱為Master/Slave), 再參考MySQL-Proxy for CentOS 5 Installation Guide安裝好一台MySQL-Proxy(後文稱為Proxy), 然後進入設定程序.

  1. 首先相關目錄

    [Proxy] # mkdir /etc/mysql-proxy/

    後續的lua檔案會放至於此
  2. 接著設定成服務, 建立一個檔案/etc/init.d/mysql-proxy, 內容如下

    #!/bin/sh
    #
    # mysql-proxy This script starts and stops the mysql-proxy daemon
    #
    # chkconfig: – 78 30
    # processname: mysql-proxy
    # description: mysql-proxy is a proxy daemon to mysql

    # Source function library.
    . /etc/rc.d/init.d/functions

    PROXY_PATH=/usr/local/bin

    prog=”mysql-proxy”

    # Source networking configuration.
    . /etc/sysconfig/network

    # Check that networking is up.
    [ ${NETWORKING} = “no” ] && exit 0

    # Source mysql-proxy configuration.
    if [ -f /etc/sysconfig/mysql-proxy ] ; then
    . /etc/sysconfig/mysql-proxy
    fi

    # Set default mysql-proxy configuration.
    PROXY_PID=/var/run/mysql-proxy.pid

    PATH=$PATH:/usr/bin:/usr/local/bin:$PROXY_PATH

    # By default it’s all good
    RETVAL=0

    # See how we were called.
    case “$1″ in
    start)
    # Start daemon.
    echo -n $”Starting $prog: ”
    daemon $NICELEVEL $PROXY_PATH/mysql-proxy $PROXY_OPTIONS $ADMIN_PLUGIN_OPTIONS $PROXY_PLUGIN_OPTIONS
    RETVAL=$?
    echo
    if [ $RETVAL = 0 ]; then
    touch /var/lock/subsys/mysql-proxy
    fi
    ;;
    stop)
    # Stop daemons.
    echo -n $”Stopping $prog: ”
    killproc $prog
    RETVAL=$?
    echo
    if [ $RETVAL = 0 ]; then
    rm -f /var/lock/subsys/mysql-proxy
    rm -f $PROXY_PID
    fi
    ;;
    restart)
    $0 stop
    sleep 3
    $0 start
    ;;
    condrestart)
    [ -e /var/lock/subsys/mysql-proxy ] && $0 restart
    ;;
    status)
    status mysql-proxy
    RETVAL=$?
    ;;
    *)
    echo “Usage: $0 {start|stop|restart|status|condrestart}”
    RETVAL=1
    ;;
    esac

    exit $RETVAL

    並且設定成可執行

    [Proxy] # chmod a+x /etc/init.d/mysql-proxy

  3. 接著在/etc/sysconfig下建立服務啟動設定檔 /etc/sysconfig/mysql-proxy, 內容如下

    # Options to mysql-proxy
    # do not remove –daemon
    LD_LIBRARY_PATH=”/usr/local/lib:$LD_LIBRARY_PATH”
    LUA_PATH=”/usr/local/lib/mysql-proxy/lua/?.lua;$LUA_PATH”
    LUA_CPATH=”/usr/local/lib/mysql-proxy/lua/?.so;$LUA_CPATH”
    export LD_LIBRARY_PATH LUA_PATH LUA_CPATH

    PROXY_OPTIONS=”–daemon”

    ADMIN_PLUGIN_OPTIONS=”–admin-address=PROXY_IP:4401 –admin-username=admin –admin-password=admin_password –admin-lua-script=/etc/mysql-proxy/admin.lua”
    PROXY_PLUGIN_OPTIONS=”–proxy-address=PROXY_IP:3306 –proxy-read-only-backend-addresses=SLAVE_IP:3306 –proxy-backend-addresses=MASTER_IP:3306 –proxy-lua-script=/etc/mysql-proxy/proxy.lua”

    請把其中的 PROXY_IP, MASTER_IP, SLAVE_IP 改成各自的ip address, 並且變更admin使用者名稱及密碼為你想要的.

  4. 建立成系統服務

    [Proxy] # /sbin/chkconfig –add mysql-proxy
    [Proxy] # /sbin/chkconfig mysql-proxy on
  5. 到MASTER及SLAVE下建立一個用來連線使用的帳號密碼, 請注意為了Replication的完整性, 請考慮在SLAVE上將該帳號的權限鎖死為僅SELECT.
  6. 設定/etc/mysql-proxy/admin.lua, 內容如下

    –[[ $%BEGINLICENSE%$
    Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved.

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License as
    published by the Free Software Foundation; version 2 of the
    License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
    02110-1301 USA

    $%ENDLICENSE%$ –]]

    function set_error(errmsg)
    proxy.response = {
    type = proxy.MYSQLD_PACKET_ERR,
    errmsg = errmsg or “error”
    }
    end

    function read_query(packet)
    if packet:byte() ~= proxy.COM_QUERY then
    set_error(“[admin] we only handle text-based queries (COM_QUERY)”)
    return proxy.PROXY_SEND_RESULT
    end

    local query = packet:sub(2)

    local rows = { }
    local fields = { }

    if query:lower() == “select * from backends” then
    fields = {
    { name = “backend_ndx”,
    type = proxy.MYSQL_TYPE_LONG },

    { name = “address”,
    type = proxy.MYSQL_TYPE_STRING },
    { name = “state”,
    type = proxy.MYSQL_TYPE_STRING },
    { name = “type”,
    type = proxy.MYSQL_TYPE_STRING },
    { name = “uuid”,
    type = proxy.MYSQL_TYPE_STRING },
    { name = “connected_clients”,
    type = proxy.MYSQL_TYPE_LONG },
    }

    for i = 1, #proxy.global.backends do
    local states = {
    “unknown”,
    “up”,
    “down”
    }
    local types = {
    “unknown”,
    “rw”,
    “ro”
    }
    local b = proxy.global.backends[i]

    rows[#rows + 1] = {
    i,
    b.dst.name, — configured backend address
    states[b.state + 1], — the C-id is pushed down starting at 0
    types[b.type + 1], — the C-id is pushed down starting at 0
    b.uuid, — the MySQL Server’s UUID if it is managed
    b.connected_clients — currently connected clients
    }
    end
    elseif query:lower() == “select * from help” then
    fields = {
    { name = “command”,
    type = proxy.MYSQL_TYPE_STRING },
    { name = “description”,
    type = proxy.MYSQL_TYPE_STRING },
    }
    rows[#rows + 1] = { “SELECT * FROM help”, “shows this help” }
    rows[#rows + 1] = { “SELECT * FROM backends”, “lists the backends and their state” }
    else
    set_error(“use ‘SELECT * FROM help’ to see the supported commands”)
    return proxy.PROXY_SEND_RESULT
    end

    proxy.response = {
    type = proxy.MYSQLD_PACKET_OK,
    resultset = {
    fields = fields,
    rows = rows
    }
    }
    return proxy.PROXY_SEND_RESULT
    end

    這個檔案可以在/usr/local/lib/mysql-proxy/lua找到, 複製過去也行, 或者可以自己寫個lua script, 這篇不是LUA語言教學, 就略過不提.

  7. 設定/etc/mysql-proxy/proxy.lua, 內容如下

    –[[ $%BEGINLICENSE%$
    Copyright (C) 2007-2008 MySQL AB, 2008 Sun Microsystems, Inc

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; version 2 of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

    $%ENDLICENSE%$ –]]


    — a flexible statement based load balancer with connection pooling

    — * build a connection pool of min_idle_connections for each backend and maintain
    — its size
    — *

    local commands = require(“proxy.commands”)
    local tokenizer = require(“proxy.tokenizer”)
    local lb = require(“proxy.balance”)
    local auto_config = require(“proxy.auto-config”)

    — config

    — connection pool
    if not proxy.global.config.rwsplit then
    proxy.global.config.rwsplit = {
    min_idle_connections = 4,
    max_idle_connections = 8,

    is_debug = false
    }
    end


    — read/write splitting sends all non-transactional SELECTs to the slaves

    — is_in_transaction tracks the state of the transactions
    local is_in_transaction = false

    — if this was a SELECT SQL_CALC_FOUND_ROWS … stay on the same connections
    local is_in_select_calc_found_rows = false


    — get a connection to a backend

    — as long as we don’t have enough connections in the pool, create new connections

    function connect_server()
    local is_debug = proxy.global.config.rwsplit.is_debug
    — make sure that we connect to each backend at least ones to
    — keep the connections to the servers alive

    — on read_query we can switch the backends again to another backend

    if is_debug then
    print()
    print(“[connect_server] ” .. proxy.connection.client.address)
    end

    local rw_ndx = 0

    — init all backends
    for i = 1, #proxy.global.backends do
    local s = proxy.global.backends[i]
    local pool = s.pool — we don’t have a username yet, try to find a connections which is idling
    local cur_idle = pool.users[“”].cur_idle_connections

    pool.min_idle_connections = proxy.global.config.rwsplit.min_idle_connections
    pool.max_idle_connections = proxy.global.config.rwsplit.max_idle_connections

    if is_debug then
    print(” [“.. i ..”].connected_clients = ” .. s.connected_clients)
    print(” [“.. i ..”].pool.cur_idle = ” .. cur_idle)
    print(” [“.. i ..”].pool.max_idle = ” .. pool.max_idle_connections)
    print(” [“.. i ..”].pool.min_idle = ” .. pool.min_idle_connections)
    print(” [“.. i ..”].type = ” .. s.type)
    print(” [“.. i ..”].state = ” .. s.state)
    end

    — prefer connections to the master
    if s.type == proxy.BACKEND_TYPE_RW and
    s.state ~= proxy.BACKEND_STATE_DOWN and
    cur_idle < pool.min_idle_connections then proxy.connection.backend_ndx = i break elseif s.type == proxy.BACKEND_TYPE_RO and s.state ~= proxy.BACKEND_STATE_DOWN and cur_idle < pool.min_idle_connections then proxy.connection.backend_ndx = i break elseif s.type == proxy.BACKEND_TYPE_RW and s.state ~= proxy.BACKEND_STATE_DOWN and rw_ndx == 0 then rw_ndx = i end end if proxy.connection.backend_ndx == 0 then if is_debug then print(" [" .. rw_ndx .. "] taking master as default") end proxy.connection.backend_ndx = rw_ndx end -- pick a random backend -- -- we someone have to skip DOWN backends -- ok, did we got a backend ? if proxy.connection.server then if is_debug then print(" using pooled connection from: " .. proxy.connection.backend_ndx) end -- stay with it return proxy.PROXY_IGNORE_RESULT end if is_debug then print(" [" .. proxy.connection.backend_ndx .. "] idle-conns below min-idle") end -- open a new connection end --- -- put the successfully authed connection into the connection pool -- -- @param auth the context information for the auth -- -- auth.packet is the packet function read_auth_result( auth ) if is_debug then print("[read_auth_result] " .. proxy.connection.client.address) end if auth.packet:byte() == proxy.MYSQLD_PACKET_OK then -- auth was fine, disconnect from the server proxy.connection.backend_ndx = 0 elseif auth.packet:byte() == proxy.MYSQLD_PACKET_EOF then -- we received either a -- -- * MYSQLD_PACKET_ERR and the auth failed or -- * MYSQLD_PACKET_EOF which means a OLD PASSWORD (4.0) was sent print("(read_auth_result) ... not ok yet"); elseif auth.packet:byte() == proxy.MYSQLD_PACKET_ERR then -- auth failed end end --- -- read/write splitting function read_query( packet ) local is_debug = proxy.global.config.rwsplit.is_debug local cmd = commands.parse(packet) local c = proxy.connection.client local r = auto_config.handle(cmd) if r then return r end local tokens local norm_query -- looks like we have to forward this statement to a backend if is_debug then print("[read_query] " .. proxy.connection.client.address) print(" current backend = " .. proxy.connection.backend_ndx) print(" client default db = " .. c.default_db) print(" client username = " .. c.username) if cmd.type == proxy.COM_QUERY then print(" query = " .. cmd.query) end end if cmd.type == proxy.COM_QUIT then -- don't send COM_QUIT to the backend. We manage the connection -- in all aspects. proxy.response = { type = proxy.MYSQLD_PACKET_OK, } if is_debug then print(" (QUIT) current backend = " .. proxy.connection.backend_ndx) end return proxy.PROXY_SEND_RESULT end proxy.queries:append(1, packet) -- read/write splitting -- -- send all non-transactional SELECTs to a slave if not is_in_transaction and cmd.type == proxy.COM_QUERY then tokens = tokens or assert(tokenizer.tokenize(cmd.query)) local stmt = tokenizer.first_stmt_token(tokens) if stmt.token_name == "TK_SQL_SELECT" then is_in_select_calc_found_rows = false local is_insert_id = false for i = 1, #tokens do local token = tokens[i] -- SQL_CALC_FOUND_ROWS + FOUND_ROWS() have to be executed -- on the same connection -- print("token: " .. token.token_name) -- print(" val: " .. token.text) if not is_in_select_calc_found_rows and token.token_name == "TK_SQL_SQL_CALC_FOUND_ROWS" then is_in_select_calc_found_rows = true elseif not is_insert_id and token.token_name == "TK_LITERAL" then local utext = token.text:upper() if utext == "LAST_INSERT_ID" or utext == "@@INSERT_ID" then is_insert_id = true end end -- we found the two special token, we can't find more if is_insert_id and is_in_select_calc_found_rows then break end end -- if we ask for the last-insert-id we have to ask it on the original -- connection if not is_insert_id then local backend_ndx = lb.idle_ro() if backend_ndx > 0 then
    proxy.connection.backend_ndx = backend_ndx
    end
    else
    print(” found a SELECT LAST_INSERT_ID(), staying on the same backend”)
    end
    end
    end

    — no backend selected yet, pick a master
    if proxy.connection.backend_ndx == 0 then
    — we don’t have a backend right now

    — let’s pick a master as a good default

    proxy.connection.backend_ndx = lb.idle_failsafe_rw()
    end

    — by now we should have a backend

    — in case the master is down, we have to close the client connections
    — otherwise we can go on
    if proxy.connection.backend_ndx == 0 then
    return proxy.PROXY_SEND_QUERY
    end

    local s = proxy.connection.server

    — if client and server db don’t match, adjust the server-side

    — skip it if we send a INIT_DB anyway
    if cmd.type ~= proxy.COM_INIT_DB and
    c.default_db and c.default_db ~= s.default_db then
    print(” server default db: ” .. s.default_db)
    print(” client default db: ” .. c.default_db)
    print(” syncronizing”)
    proxy.queries:prepend(2, string.char(proxy.COM_INIT_DB) .. c.default_db)
    end

    — send to master
    if is_debug then
    if proxy.connection.backend_ndx > 0 then
    local b = proxy.global.backends[proxy.connection.backend_ndx]
    print(” sending to backend : ” .. b.address);
    print(” is_slave : ” .. tostring(b.type == proxy.BACKEND_TYPE_RO));
    print(” server default db: ” .. s.default_db)
    print(” server username : ” .. s.username)
    end
    print(” in_trans : ” .. tostring(is_in_transaction))
    print(” in_calc_found : ” .. tostring(is_in_select_calc_found_rows))
    print(” COM_QUERY : ” .. tostring(cmd.type == proxy.COM_QUERY))
    end

    return proxy.PROXY_SEND_QUERY
    end


    — as long as we are in a transaction keep the connection
    — otherwise release it so another client can use it
    function read_query_result( inj )
    local is_debug = proxy.global.config.rwsplit.is_debug
    local res = assert(inj.resultset)
    local flags = res.flags

    if inj.id ~= 1 then
    — ignore the result of the USE
    — the DB might not exist on the backend, what do do ?

    if inj.id == 2 then
    — the injected INIT_DB failed as the slave doesn’t have this DB
    — or doesn’t have permissions to read from it
    if res.query_status == proxy.MYSQLD_PACKET_ERR then
    proxy.queries:reset()
    proxy.response = {
    type = proxy.MYSQLD_PACKET_ERR,
    errmsg = “can’t change DB “.. proxy.connection.client.default_db ..
    ” to on slave ” .. proxy.global.backends[proxy.connection.backend_ndx].address
    }

    return proxy.PROXY_SEND_RESULT
    end
    end
    return proxy.PROXY_IGNORE_RESULT
    end

    is_in_transaction = flags.in_trans
    local have_last_insert_id = (res.insert_id and (res.insert_id > 0))

    if not is_in_transaction and
    not is_in_select_calc_found_rows and
    not have_last_insert_id then
    — release the backend
    proxy.connection.backend_ndx = 0
    elseif is_debug then
    print(“(read_query_result) staying on the same backend”)
    print(” in_trans : ” .. tostring(is_in_transaction))
    print(” in_calc_found : ” .. tostring(is_in_select_calc_found_rows))
    print(” have_insert_id : ” .. tostring(have_last_insert_id))
    end
    end


    — close the connections if we have enough connections in the pool

    — @return nil – close connection
    — IGNORE_RESULT – store connection in the pool
    function disconnect_client()
    local is_debug = proxy.global.config.rwsplit.is_debug
    if is_debug then
    print(“[disconnect_client] ” .. proxy.connection.client.address)
    end

    — make sure we are disconnection from the connection
    — to move the connection into the pool
    proxy.connection.backend_ndx = 0
    end

    這個檔案在MySQL-Proxy的repository找的到, 是標準的Read/Write分離, 請自行Google

  8. 接著啟動MySQL-Proxy

    [Proxy] # /sbin/service mysql-proxy start

    連接上Admin Console

    [Proxy] # mysql -u admin -padmin_password -h PROXY_IP -P 4401
    Welcome to the MySQL monitor. Commands end with ; or \g.
    Your MySQL connection id is 1
    Server version: 5.0.99-agent-admin

    Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved.
    This software comes with ABSOLUTELY NO WARRANTY. This is free software,
    and you are welcome to modify and redistribute it under the GPL v2 license

    Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.

    mysql> SELECT * FROM backends;
    +————-+——————–+———+——+——+——————-+
    | backend_ndx | address | state | type | uuid | connected_clients |
    +————-+——————–+———+——+——+——————-+
    | 1 | MASTER_IP:3306 | unknown | rw | NULL | 0 |
    | 2 | SLAVE_IP:3306 | unknown | ro | NULL | 0 |
    +————-+——————–+———+——+——+——————-+
    2 rows in set (0.00 sec)

    或者是直接用設定在MASTER與SLAVE上相同的mysql使用者帳密透過proxy連接

    [Proxy] # mysql -u USERNAME -pPASWORD -h PROXY_IP -P 3306
    Welcome to the MySQL monitor. Commands end with ; or \g.
    Your MySQL connection id is 162
    Server version: 5.0.77-log Source distribution

    Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved.
    This software comes with ABSOLUTELY NO WARRANTY. This is free software,
    and you are welcome to modify and redistribute it under the GPL v2 license

    Type ‘help;’ or ‘\h’ for help. Type ‘\c’ to clear the current input statement.

    mysql>

    這樣就可以正常使用了

一樣, 這只是個clean install/configure的run down, 碰到問題請詢問Google大人. 雖然那堆config很吃篇幅, 不過我相信有人用的到, 避免每個人Google到不同的東西, 所以寫個清楚.

MySQL Replication (Master-Slave) for CentOS Installation Guide

這個是承上一篇, 裝完MySQL-Proxy後, 後續怎麼從零建置起, 第一步要先把後端的MySQL Replication建立起來.

準備好兩台機器(目前稱為Master/Slave), 首先安裝好CentOS 5之後, 一樣先把Repositories都裝好, 也都update/upgrade好, 以下是兩台共通的部份.

  1. 安裝一套CentOS5, 我實驗的VM是使用CentOS 5.6, 只裝了基本的Base跟Develop Tools
  2. 先做一次yum -y update, 更新一些套件到最新
  3. 安裝RPMForge & EPEL Repositories
  4. 再做一次yum -y update, 更新一些套件到最新
  5. 安裝MySQL Server, 設定好services

    # yum -y install mysql-server
    # /sbin/chkconfig mysqld on
    # /sbin/service mysqld start
  6. 設定iptables, Master/Slave互相開放對方的TCP 3306 port, 其中eth0改成自己的網卡代號.

    # /sbin/iptables -A INPUT -i eth0 -p tcp -m tcp –dport 3306 -j ACCEPT

接著是兩台不同的部份, 請注意前面的Hostname代表是在哪一台上

  1. 到Master上先設定my.cnf, 這個是可以選擇不同設定檔, 在/usr/share/mysql 下有很多個, 這裡挑選medium的, 細部設定請參考mysql網站上的文件, 這裡直接用medium的標準設定.

    [Master] # /sbin/services mysqld stop
    [Master] # cp /usr/share/mysql/my-medium.cnf /etc/my.cnf
  2. 設定 /etc/my.cnf, 務必打開log-bin

    server-id = 1
    log-bin = mysql-bin

    其中server-id必須各自不同, 用於Replication關係中識別各Server.
  3. 設定Replication使用的連線帳號

    [Master] # /sbin/service mysqld start
    [Master] # mysql -u root
    mysql> GRANT REPLICATION SLAVE ON *.* TO repl@SLAVE-IP IDENTIFIED BY ‘repl_password’;
    mysql> FLUSH PRIVILEGES;

    其中 repl 是Slave用來連接Master使用的帳號, SLAVE-IP是Slave的ip address, 增加一個slave時必須重加, repl_password則是Slave連接Master時的密碼, 請更換成自己的.
  4. 接著把Master的資料都Lock, 避免任何新資料寫入, 並且取得Master status

    mysql> FLUSH TABLES WITH READ LOCK;
    mysql> SHOW MASTER STATUS;
    +——————+———-+————–+——————+
    | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
    +——————+———-+————–+——————+
    | mysql-bin.000001 | 759 | | |
    +——————+———-+————–+——————+

    上頭的File跟Position就是等等Slave設定時使用的參數
  5. 到Slave上, 一樣先複製一份my.cnf

    [Slave] # /sbin/service mysqld stop
    [Slave] # cp /usr/share/mysql/my-medium.cnf /etc/my.cnf
  6. 設定my.cnf, 唯一的不同是server-id一定要跟Master不同, 而且不能跟其他Slave重複

    server-id = 2
    log-bin = mysql-bin

    server-id務必與Master不同, 也不能與其他Slave(假如有的話)相同
  7. 啟動Slave並設定Master相關資訊

    [Slave] # /sbin/services mysqld start
    [Slave] # mysql -u root
    mysql> CHANGE MASTER TO
    -> MASTER_HOST=Master-IP,
    -> MASTER_USER=’repl’,
    -> MASTER_PASSWORD=’repl_password’,
    -> MASTER_LOG_FILE=’mysql-bin.000001′,
    -> MASTER_LOG_POS=’759′;

    其中Master-IP則為Master的IP, repl是剛剛設定在Master上的帳號, MASTER_LOG_FILE則是剛剛在Master上SHOW Master Status出現的Binary Log檔名, MASTER_LOG_POS則是現在在Binary Log檔案中的位置, 請依照前面看到的填入.
  8. 在Slave上啟動Slave 機制

    mysql> Start Slave;
    Query OK, 0 rows affected, 0 warning (0.00 sec)

    這樣就表示成功了, 也可以在Slave上SHOW SLAVE STATUS來看狀態

    mysql> SHOW SLAVE STATUS\G
    *************************** 1. row ***************************
    Slave_IO_State: Waiting for master to send event
    Master_Host: xxx.xxx.xxx.xxx
    Master_User: repl
    Master_Port: 3306
    Connect_Retry: 60
    Master_Log_File: mysql-bin.000001
    Read_Master_Log_Pos: 759
    Relay_Log_File: mysqld-relay-bin.000042
    Relay_Log_Pos: 235
    Relay_Master_Log_File: mysql-bin.000001
    Slave_IO_Running: Yes
    Slave_SQL_Running: Yes
    Replicate_Do_DB:
    Replicate_Ignore_DB:
    Replicate_Do_Table:
    Replicate_Ignore_Table:
    Replicate_Wild_Do_Table:
    Replicate_Wild_Ignore_Table:
    Last_Errno: 0
    Last_Error:
    Skip_Counter: 0
    Exec_Master_Log_Pos: 759
    Relay_Log_Space: 235
    Until_Condition: None
    Until_Log_File:
    Until_Log_Pos: 0
    Master_SSL_Allowed: No
    Master_SSL_CA_File:
    Master_SSL_CA_Path:
    Master_SSL_Cert:
    Master_SSL_Cipher:
    Master_SSL_Key:
    Seconds_Behind_Master: 0
    1 row in set (0.00 sec)

    主要要看到Slave_SQL_Running是Yes就大致ok了.
  9. 最後回到Master上, 把剛剛Lock住的解開

    [Master] # mysql -u root
    mysql> UNLOCK TABLES;

    接下來就可以實驗新增Db/Tables/Rows看看有沒有被Replication到Slave去.

大致這樣就乾淨的把兩台機器之間的MySQL Replication設定好了, 但是還有一些注意事項

  1. 絕對不要在Slave上寫入或更新任何資料, 必要時甚至只開啟Select的權限
  2. 必須定期檢查Slave上的Replication狀態, 是否有錯誤
  3. 由於開啟Binary Log後, 忙碌的MySQL Server會不停的佔用空間, 也請在確認好Replication狀態運行中的時候, 定期PURGE binary log, 相關文件請參考 這裡
  4. 若Replication狀態損壞時, 需要重建關係, 在Master Lock之後, 把mysql data file複製一份至Slave後, 程序相同

這裡只有簡單的run down, 中間碰到任何問題請Google解決吧.

MySQL-Proxy for CentOS 5 Installation Guide

雖說CentOS 5在 EPEL Repository有mysql-proxy, 但是那是0.5.x 的版本, 要用比較新的0.8.x, 目前沒有看到, 所以花時間去試了一下.

  1. 安裝一套CentOS5, 我實驗的VM是使用CentOS 5.6, 只裝了基本的Base跟Develop Tools
  2. 先做一次yum -y update, 更新一些套件到最新
  3. 安裝RPMForge & EPEL Repositories
  4. 再做一次yum -y update, 更新一些套件到最新
  5. 安裝一些必須的套件:

    yum -y install install gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel openldap openldap-devel nss_ldap openldap-clients openldap-servers flex libtiff-devel pam-devel pkgconfig readline-devel zlib-devel libevent-devel
  6. libevent網站抓libevent來裝, CentOS附的太舊

    # wget http://monkey.org/~provos/libevent-2.0.12-stable.tar.gz
    # tar -zxpvf libevent-2.0.12-stable.tar.gz
    # cd libevent-2.0.12-stable
    # ./configure –prefix=/usr/local
    # make all install
  7. LUA網站抓新的LUA來裝, CentOS附的太舊

    # wget http://www.lua.org/ftp/lua-5.1.4.tar.gz
    # tar -zxpvf lua-5.1.4.tar.gz
    # cd lua-5.1.4
    # vi src/Makefile

    修改CFLAGS, 在後頭加入 -fPIC

    # make linux install
    # cp src/lua.pc /usr/local/lib/pkgconfig

  8. 這裡 自己複製一份magic.h, 放置到 /usr/include/linux/magic.h, 後頭裝glib2會用到
  9. GTK+網站抓glib2回來裝, CentOS附的太舊

    # wget http://ftp.gnome.org/pub/gnome/sources/glib/2.28/glib-2.28.0.tar.bz2
    # bzip2 -d -c glib-2.28.0.tar.bz2 | tar -xpvf –
    # cd glib-2.28.0
    # ./configure –prefix=/usr/local
    # make all install
  10. 要Build mysql-proxy 之前先設定好一些環境變數, 否則會去用到CentOS內附的舊版本

    # export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
    # export LUA_CFLAGS=”-I/usr/local/include” LUA_LIBS=”-L/usr/local/lib -llua -ldl” LDFLAGS=”-lm”
    # export GLIB_CFLAGS=”-I/usr/local/include/glib-2.0 -I/usr/local/lib/glib-2.0/include”
    # export GLIB_LIBS=”-L/usr/local/lib -lglib-2.0″
    # export GMODULE_CFLAGS=”-I/usr/local/include/glib-2.0 -I/usr/local/lib/glib-2.0/include”
    # export GMODULE_LIBS=”-L/usr/local/lib -lgmodule-2.0″
  11. 終於可以開始build mysql-proxy了, 去 MySQL Proxy網站抓Source, 這裡有Mirror.

    # wget http://mysql.cdpa.nsysu.edu.tw/Downloads/MySQL-Proxy/mysql-proxy-0.8.1.tar.gz
    # tar -zxpvf mysql-proxy-0.8.1.tar.gz
    # cd mysql-proxy-0.8.1
    # ./configure –prefix=/usr/local
    # make all install

  12. 這樣就完成了, 可以執行mysql-proxy -V 確認, 以下是我跑出來的結果

    # /usr/local/bin/mysql-proxy -V
    mysql-proxy 0.8.1
    chassis: mysql-proxy 0.8.1
    glib2: 2.28.0
    libevent: 2.0.12-stable
    LUA: Lua 5.1.4
    package.path: /usr/local/lib/mysql-proxy/lua/?.lua
    package.cpath: /usr/local/lib/mysql-proxy/lua/?.so
    — modules
    admin: 0.8.1
    proxy: 0.8.1

至於裝好之後的設定, 不是這篇要講的, 這裡幫大家裝起來就好 🙂