1、服務(wù)器編程:以前你如果使用C或者C++做的那些事情,用Go來(lái)做很合適,例如處理日志、數(shù)據(jù)打包、虛擬機(jī)處理、文件系統(tǒng)等。
創(chuàng)新互聯(lián)網(wǎng)絡(luò)公司擁有10年的成都網(wǎng)站開(kāi)發(fā)建設(shè)經(jīng)驗(yàn),上千余家客戶的共同信賴。提供網(wǎng)站設(shè)計(jì)、成都網(wǎng)站制作、網(wǎng)站開(kāi)發(fā)、網(wǎng)站定制、賣友情鏈接、建網(wǎng)站、網(wǎng)站搭建、成都響應(yīng)式網(wǎng)站建設(shè)公司、網(wǎng)頁(yè)設(shè)計(jì)師打造企業(yè)風(fēng)格,提供周到的售前咨詢和貼心的售后服務(wù)
2、分布式系統(tǒng)、數(shù)據(jù)庫(kù)代理器、中間件:例如Etcd。
3、網(wǎng)絡(luò)編程:這一塊目前應(yīng)用最廣,包括Web應(yīng)用、API應(yīng)用、下載應(yīng)用,而且Go內(nèi)置的net/http包基本上把我們平常用到的網(wǎng)絡(luò)功能都實(shí)現(xiàn)了。
4、開(kāi)發(fā)云平臺(tái):目前國(guó)外很多云平臺(tái)在采用Go開(kāi)發(fā),我們所熟知的七牛云、華為云等等都有使用Go進(jìn)行開(kāi)發(fā)并且開(kāi)源的成型的產(chǎn)品。
5、區(qū)塊鏈:目前有一種說(shuō)法,技術(shù)從業(yè)人員把Go語(yǔ)言稱作為區(qū)塊鏈行業(yè)的開(kāi)發(fā)語(yǔ)言。如果大家學(xué)習(xí)區(qū)塊鏈技術(shù)的話,就會(huì)發(fā)現(xiàn)現(xiàn)在有很多很多的區(qū)塊鏈的系統(tǒng)和應(yīng)用都是采用Go進(jìn)行開(kāi)發(fā)的,比如ehtereum是目前知名度最大的公鏈,再比如fabric是目前最知名的聯(lián)盟鏈,兩者都有g(shù)o語(yǔ)言的版本,且go-ehtereum還是以太坊官方推薦的版本。
自1.0版發(fā)布以來(lái),go語(yǔ)言引起了眾多開(kāi)發(fā)者的關(guān)注,并得到了廣泛的應(yīng)用。go語(yǔ)言簡(jiǎn)單、高效、并發(fā)的特點(diǎn)吸引了許多傳統(tǒng)的語(yǔ)言開(kāi)發(fā)人員,其數(shù)量也在不斷增加。
使用 Go 語(yǔ)言開(kāi)發(fā)的開(kāi)源項(xiàng)目非常多。早期的 Go 語(yǔ)言開(kāi)源項(xiàng)目只是通過(guò) Go 語(yǔ)言與傳統(tǒng)項(xiàng)目進(jìn)行C語(yǔ)言庫(kù)綁定實(shí)現(xiàn),例如 Qt、Sqlite 等。
后期的很多項(xiàng)目都使用 Go 語(yǔ)言進(jìn)行重新原生實(shí)現(xiàn),這個(gè)過(guò)程相對(duì)于其他語(yǔ)言要簡(jiǎn)單一些,這也促成了大量使用 Go 語(yǔ)言原生開(kāi)發(fā)項(xiàng)目的出現(xiàn)。
Go 語(yǔ)言被設(shè)計(jì)成一門(mén)應(yīng)用于搭載 Web 服務(wù)器,存儲(chǔ)集群或類似用途的巨型中央服務(wù)器的系統(tǒng)編程語(yǔ)言。對(duì)于高性能分布式系統(tǒng)領(lǐng)域而言,Go 語(yǔ)言無(wú)疑比大多數(shù)其它語(yǔ)言有著更高的開(kāi)發(fā)效率。學(xué)習(xí)Go語(yǔ)言,可以說(shuō)是很簡(jiǎn)單的,入門(mén)快,想學(xué)習(xí)Go語(yǔ)言,可以到黑馬程序員看看,有新出的教程。
簡(jiǎn)單減少slave同步延案架構(gòu)做優(yōu)化盡量讓主庫(kù)DDL快速執(zhí)行主庫(kù)寫(xiě)數(shù)據(jù)安全性較高比sync_binlog=1innodb_flush_log_at_trx_commit = 1 類設(shè)置slave則需要高數(shù)據(jù)安全完全講sync_binlog設(shè)置0或者關(guān)閉binloginnodb_flushlog設(shè)置0提高sql執(zhí)行效率另外使用比主庫(kù)更硬件設(shè)備作slave
mysql-5.6.3已經(jīng)支持線程主復(fù)制原理丁奇類似丁奇表做線程O(píng)racle使用數(shù)據(jù)庫(kù)(schema)單位做線程同庫(kù)使用同復(fù)制線程
sync_binlog=1
This makes MySQL synchronize the binary log’s contents to disk each time it commits a transaction
默認(rèn)情況并每寫(xiě)入都binlog與硬盤(pán)同步操作系統(tǒng)或機(jī)器(僅僅MySQL服務(wù)器)崩潰能binlog語(yǔ)句丟 失要想防止種情況使用sync_binlog全局變量(1安全值慢)使binlog每Nbinlog寫(xiě)入與硬盤(pán) 同步即使sync_binlog設(shè)置1,現(xiàn)崩潰能表內(nèi)容binlog內(nèi)容間存致性使用InnoDB表MySQL服務(wù)器 處理COMMIT語(yǔ)句整事務(wù)寫(xiě)入binlog并事務(wù)提交InnoDB兩操作間現(xiàn)崩潰重啟事務(wù)InnoDB滾仍 存binlog用--innodb-safe-binlog選項(xiàng)增加InnoDB表內(nèi)容binlog間致性(注釋:MySQL 5.1需要--innodb-safe-binlog;由于引入XA事務(wù)支持該選項(xiàng)作廢)該選項(xiàng)提供更程度安全使每事務(wù) binlog(sync_binlog =1)(默認(rèn)情況真)InnoDB志與硬盤(pán)同步該選項(xiàng)效崩潰重啟滾事務(wù)MySQL服務(wù)器binlog剪切滾 InnoDB事務(wù)確保binlog反饋InnoDB表確切數(shù)據(jù)等并使服務(wù)器保持與主服務(wù)器保持同步(接收 滾語(yǔ)句)
innodb_flush_log_at_trx_commit (管用)
抱怨Innodb比MyISAM慢 100倍概忘調(diào)整值默認(rèn)值1意思每事務(wù)提交或事務(wù)外指令都需要志寫(xiě)入(flush)硬盤(pán)費(fèi)特別使用電 池供電緩存(Battery backed up cache)設(shè)2于運(yùn)用特別MyISAM表轉(zhuǎn)意思寫(xiě)入硬盤(pán)寫(xiě)入系統(tǒng)緩存志仍每秒flush硬 盤(pán)所般丟失超1-2秒更新設(shè)0更快點(diǎn)安全面比較差即使MySQL掛能丟失事務(wù)數(shù)據(jù)值2整操作系統(tǒng) 掛才能丟數(shù)據(jù)
在Malwarebytes 我們經(jīng)歷了顯著的增長(zhǎng),自從我一年前加入了硅谷的公司,一個(gè)主要的職責(zé)成了設(shè)計(jì)架構(gòu)和開(kāi)發(fā)一些系統(tǒng)來(lái)支持一個(gè)快速增長(zhǎng)的信息安全公司和所有需要的設(shè)施來(lái)支持一個(gè)每天百萬(wàn)用戶使用的產(chǎn)品。我在反病毒和反惡意軟件行業(yè)的不同公司工作了12年,從而我知道由于我們每天處理大量的數(shù)據(jù),這些系統(tǒng)是多么復(fù)雜。
有趣的是,在過(guò)去的大約9年間,我參與的所有的web后端的開(kāi)發(fā)通常是通過(guò)Ruby on Rails技術(shù)實(shí)現(xiàn)的。不要錯(cuò)怪我。我喜歡Ruby on Rails,并且我相信它是個(gè)令人驚訝的環(huán)境。但是一段時(shí)間后,你會(huì)開(kāi)始以ruby的方式開(kāi)始思考和設(shè)計(jì)系統(tǒng),你會(huì)忘記,如果你可以利用多線程、并行、快速執(zhí)行和小內(nèi)存開(kāi)銷,軟件架構(gòu)本來(lái)應(yīng)該是多么高效和簡(jiǎn)單。很多年期間,我是一個(gè)c/c++、Delphi和c#開(kāi)發(fā)者,我剛開(kāi)始意識(shí)到使用正確的工具可以把復(fù)雜的事情變得簡(jiǎn)單些。
作為首席架構(gòu)師,我不會(huì)很關(guān)心在互聯(lián)網(wǎng)上的語(yǔ)言和框架戰(zhàn)爭(zhēng)。我相信效率、生產(chǎn)力。代碼可維護(hù)性主要依賴于你如何把解決方案設(shè)計(jì)得很簡(jiǎn)單。
問(wèn)題
當(dāng)工作在我們的匿名遙測(cè)和分析系統(tǒng)中,我們的目標(biāo)是可以處理來(lái)自于百萬(wàn)級(jí)別的終端的大量的POST請(qǐng)求。web處理服務(wù)可以接收包含了很多payload的集合的JSON數(shù)據(jù),這些數(shù)據(jù)需要寫(xiě)入Amazon S3中。接下來(lái),map-reduce系統(tǒng)可以操作這些數(shù)據(jù)。
按照習(xí)慣,我們會(huì)調(diào)研服務(wù)層級(jí)架構(gòu),涉及的軟件如下:
Sidekiq
Resque
DelayedJob
Elasticbeanstalk Worker Tier
RabbitMQ
and so on…
搭建了2個(gè)不同的集群,一個(gè)提供web前端,另外一個(gè)提供后端處理,這樣我們可以橫向擴(kuò)展后端服務(wù)的數(shù)量。
但是,從剛開(kāi)始,在 討論階段我們的團(tuán)隊(duì)就知道我們應(yīng)該使用Go,因?yàn)槲覀兛吹竭@會(huì)潛在性地成為一個(gè)非常龐大( large traffic)的系統(tǒng)。我已經(jīng)使用了Go語(yǔ)言大約2年時(shí)間,我們開(kāi)發(fā)了幾個(gè)系統(tǒng),但是很少會(huì)達(dá)到這樣的負(fù)載(amount of load)。
我們開(kāi)始創(chuàng)建一些結(jié)構(gòu),定義從POST調(diào)用得到的web請(qǐng)求負(fù)載,還有一個(gè)上傳到S3 budket的函數(shù)。
type PayloadCollection struct {
WindowsVersion string `json:"version"`
Token string `json:"token"`
Payloads []Payload `json:"data"`
}
type Payload struct {
// [redacted]
}
func (p *Payload) UploadToS3() error {
// the storageFolder method ensures that there are no name collision in
// case we get same timestamp in the key name
storage_path := fmt.Sprintf("%v/%v", p.storageFolder, time.Now().UnixNano())
bucket := S3Bucket
b := new(bytes.Buffer)
encodeErr := json.NewEncoder(b).Encode(payload)
if encodeErr != nil {
return encodeErr
}
// Everything we post to the S3 bucket should be marked 'private'
var acl = s3.Private
var contentType = "application/octet-stream"
return bucket.PutReader(storage_path, b, int64(b.Len()), contentType, acl, s3.Options{})
}
本地Go routines方法
剛開(kāi)始,我們采用了一個(gè)非常本地化的POST處理實(shí)現(xiàn),僅僅嘗試把發(fā)到簡(jiǎn)單go routine的job并行化:
func payloadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Read the body into a string for json decoding
var content = PayloadCollection{}
err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(content)
if err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusBadRequest)
return
}
// Go through each payload and queue items individually to be posted to S3
for _, payload := range content.Payloads {
go payload.UploadToS3() // ----- DON'T DO THIS
}
w.WriteHeader(http.StatusOK)
}
對(duì)于中小負(fù)載,這會(huì)對(duì)大多數(shù)的人適用,但是大規(guī)模下,這個(gè)方案會(huì)很快被證明不是很好用。我們期望的請(qǐng)求數(shù),不在我們剛開(kāi)始計(jì)劃的數(shù)量級(jí),當(dāng)我們把第一個(gè)版本部署到生產(chǎn)環(huán)境上。我們完全低估了流量。
上面的方案在很多地方很不好。沒(méi)有辦法控制我們產(chǎn)生的go routine的數(shù)量。由于我們收到了每分鐘1百萬(wàn)的POST請(qǐng)求,這段代碼很快就崩潰了。
再次嘗試
我們需要找一個(gè)不同的方式。自開(kāi)始我們就討論過(guò), 我們需要保持請(qǐng)求處理程序的生命周期很短,并且進(jìn)程在后臺(tái)產(chǎn)生。當(dāng)然,這是你在Ruby on Rails的世界里必須要做的事情,否則你會(huì)阻塞在所有可用的工作 web處理器上,不管你是使用puma、unicore還是passenger(我們不要討論JRuby這個(gè)話題)。然后我們需要利用常用的處理方案來(lái)做這些,比如Resque、 Sidekiq、 SQS等。這個(gè)列表會(huì)繼續(xù)保留,因?yàn)橛泻芏嗟姆桨缚梢詫?shí)現(xiàn)這些。
所以,第二次迭代,我們創(chuàng)建了一個(gè)緩沖channel,我們可以把job排隊(duì),然后把它們上傳到S3。因?yàn)槲覀兛梢钥刂莆覀冴?duì)列中的item最大值,我們有大量的內(nèi)存來(lái)排列job,我們認(rèn)為只要把job在channel里面緩沖就可以了。
var Queue chan Payload
func init() {
Queue = make(chan Payload, MAX_QUEUE)
}
func payloadHandler(w http.ResponseWriter, r *http.Request) {
...
// Go through each payload and queue items individually to be posted to S3
for _, payload := range content.Payloads {
Queue - payload
}
...
}
接下來(lái),我們?cè)購(gòu)年?duì)列中取job,然后處理它們。我們使用類似于下面的代碼:
func StartProcessor() {
for {
select {
case job := -Queue:
job.payload.UploadToS3() // -- STILL NOT GOOD
}
}
}
說(shuō)實(shí)話,我不知道我們?cè)谙胧裁?。這肯定是一個(gè)滿是Red-Bulls的夜晚。這個(gè)方法不會(huì)帶來(lái)什么改善,我們用了一個(gè) 有缺陷的緩沖隊(duì)列并發(fā),僅僅是把問(wèn)題推遲了。我們的同步處理器同時(shí)僅僅會(huì)上傳一個(gè)數(shù)據(jù)到S3,因?yàn)閬?lái)到的請(qǐng)求遠(yuǎn)遠(yuǎn)大于單核處理器上傳到S3的能力,我們的帶緩沖channel很快達(dá)到了它的極限,然后阻塞了請(qǐng)求處理邏輯的queue更多item的能力。
我們僅僅避免了問(wèn)題,同時(shí)開(kāi)始了我們的系統(tǒng)掛掉的倒計(jì)時(shí)。當(dāng)部署了這個(gè)有缺陷的版本后,我們的延時(shí)保持在每分鐘以常量增長(zhǎng)。
最好的解決方案
我們討論過(guò)在使用用Go channel時(shí)利用一種常用的模式,來(lái)創(chuàng)建一個(gè)二級(jí)channel系統(tǒng),一個(gè)來(lái)queue job,另外一個(gè)來(lái)控制使用多少個(gè)worker來(lái)并發(fā)操作JobQueue。
想法是,以一個(gè)恒定速率并行上傳到S3,既不會(huì)導(dǎo)致機(jī)器崩潰也不好產(chǎn)生S3的連接錯(cuò)誤。這樣我們選擇了創(chuàng)建一個(gè)Job/Worker模式。對(duì)于那些熟悉Java、C#等語(yǔ)言的開(kāi)發(fā)者,可以把這種模式想象成利用channel以golang的方式來(lái)實(shí)現(xiàn)了一個(gè)worker線程池,作為一種替代。
var (
MaxWorker = os.Getenv("MAX_WORKERS")
MaxQueue = os.Getenv("MAX_QUEUE")
)
// Job represents the job to be run
type Job struct {
Payload Payload
}
// A buffered channel that we can send work requests on.
var JobQueue chan Job
// Worker represents the worker that executes the job
type Worker struct {
WorkerPool chan chan Job
JobChannel chan Job
quit chan bool
}
func NewWorker(workerPool chan chan Job) Worker {
return Worker{
WorkerPool: workerPool,
JobChannel: make(chan Job),
quit: make(chan bool)}
}
// Start method starts the run loop for the worker, listening for a quit channel in
// case we need to stop it
func (w Worker) Start() {
go func() {
for {
// register the current worker into the worker queue.
w.WorkerPool - w.JobChannel
select {
case job := -w.JobChannel:
// we have received a work request.
if err := job.Payload.UploadToS3(); err != nil {
log.Errorf("Error uploading to S3: %s", err.Error())
}
case -w.quit:
// we have received a signal to stop
return
}
}
}()
}
// Stop signals the worker to stop listening for work requests.
func (w Worker) Stop() {
go func() {
w.quit - true
}()
}
我們已經(jīng)修改了我們的web請(qǐng)求handler,用payload創(chuàng)建一個(gè)Job實(shí)例,然后發(fā)到JobQueue channel,以便于worker來(lái)獲取。
func payloadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// Read the body into a string for json decoding
var content = PayloadCollection{}
err := json.NewDecoder(io.LimitReader(r.Body, MaxLength)).Decode(content)
if err != nil {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusBadRequest)
return
}
// Go through each payload and queue items individually to be posted to S3
for _, payload := range content.Payloads {
// let's create a job with the payload
work := Job{Payload: payload}
// Push the work onto the queue.
JobQueue - work
}
w.WriteHeader(http.StatusOK)
}
在web server初始化時(shí),我們創(chuàng)建一個(gè)Dispatcher,然后調(diào)用Run()函數(shù)創(chuàng)建一個(gè)worker池子,然后開(kāi)始監(jiān)聽(tīng)JobQueue中的job。
dispatcher := NewDispatcher(MaxWorker)
dispatcher.Run()
下面是dispatcher的實(shí)現(xiàn)代碼:
type Dispatcher struct {
// A pool of workers channels that are registered with the dispatcher
WorkerPool chan chan Job
}
func NewDispatcher(maxWorkers int) *Dispatcher {
pool := make(chan chan Job, maxWorkers)
return Dispatcher{WorkerPool: pool}
}
func (d *Dispatcher) Run() {
// starting n number of workers
for i := 0; i d.maxWorkers; i++ {
worker := NewWorker(d.pool)
worker.Start()
}
go d.dispatch()
}
func (d *Dispatcher) dispatch() {
for {
select {
case job := -JobQueue:
// a job request has been received
go func(job Job) {
// try to obtain a worker job channel that is available.
// this will block until a worker is idle
jobChannel := -d.WorkerPool
// dispatch the job to the worker job channel
jobChannel - job
}(job)
}
}
}
注意到,我們提供了初始化并加入到池子的worker的最大數(shù)量。因?yàn)檫@個(gè)工程我們利用了Amazon Elasticbeanstalk帶有的docker化的Go環(huán)境,所以我們常常會(huì)遵守12-factor方法論來(lái)配置我們的生成環(huán)境中的系統(tǒng),我們從環(huán)境變了讀取這些值。這種方式,我們控制worker的數(shù)量和JobQueue的大小,所以我們可以很快的改變這些值,而不需要重新部署集群。
var (
MaxWorker = os.Getenv("MAX_WORKERS")
MaxQueue = os.Getenv("MAX_QUEUE")
)
直接結(jié)果
我們部署了之后,立馬看到了延時(shí)降到微乎其微的數(shù)值,并未我們處理請(qǐng)求的能力提升很大。
Elastic Load Balancers完全啟動(dòng)后,我們看到ElasticBeanstalk 應(yīng)用服務(wù)于每分鐘1百萬(wàn)請(qǐng)求。通常情況下在上午時(shí)間有幾個(gè)小時(shí),流量峰值超過(guò)每分鐘一百萬(wàn)次。
我們一旦部署了新的代碼,服務(wù)器的數(shù)量從100臺(tái)大幅 下降到大約20臺(tái)。
我們合理配置了我們的集群和自動(dòng)均衡配置之后,我們可以把服務(wù)器的數(shù)量降至4x EC2 c4.Large實(shí)例,并且Elastic Auto-Scaling設(shè)置為如果CPU達(dá)到5分鐘的90%利用率,我們就會(huì)產(chǎn)生新的實(shí)例。
總結(jié)
在我的書(shū)中,簡(jiǎn)單總是獲勝。我們可以使用多隊(duì)列、后臺(tái)worker、復(fù)雜的部署設(shè)計(jì)一個(gè)復(fù)雜的系統(tǒng),但是我們決定利用Elasticbeanstalk 的auto-scaling的能力和Go語(yǔ)言開(kāi)箱即用的特性簡(jiǎn)化并發(fā)。
我們僅僅用了4臺(tái)機(jī)器,這并不是什么新鮮事了??赡芩鼈冞€不如我的MacBook能力強(qiáng)大,但是卻處理了每分鐘1百萬(wàn)的寫(xiě)入到S3的請(qǐng)求。
處理問(wèn)題有正確的工具。當(dāng)你的 Ruby on Rails 系統(tǒng)需要更強(qiáng)大的web handler時(shí),可以考慮下ruby生態(tài)系統(tǒng)之外的技術(shù),或許可以得到更簡(jiǎn)單但更強(qiáng)大的替代方案。