CT Wu

Software Architect · Backend · Data Engineering

Common interface for query functions in sql.DB and sql.Tx

I want to implement a command pattern upon the database/sql in golang. The goal is that no matter whether the client uses sql.DB or sql.Tx, it can get the result by executing the same command. So that, the client can decide which instance to pass the command according to its needs. The problem is golang is a strongly typed language; therefore, you have to define the instance type in command’s struct first. They are two different types(sql.DB and sql.Tx), and it bothers me.

There is an interesting discussion on Github of the golang standard library. I am looking for the same solution like the issue author’s proposal; thus, I read the whole posts in the discussion.

I extract two important points:

  1. Many people have the same requirement, so the issue is more like a common abstraction to the high-level design. Libraries are used to ease up things and to not make everyone repeat the very same work.
  2. Does such an interface need to live in the standard library? It is more like a design concept than a driver.

Originally, my thoughts were biased towards the former and then I felt that the latter was more correct. From my point of view, the common platform cannot meet everyone’s design concept, that is not practical. If there is indeed a design need, which means it have to be accomplished by a new high-level library. In this case, sqlexp has already played this role. We can either buy in it or create another one.

Originally published on Medium

Make SQL Scan result be map in golang

When we are using MySQL in golang, we usually adopt database/sql to finish our jobs. In my opinion, the most common driver must be go-sql-driver. It is efficient and easy to use but hard to use well.

Especially, the fetch result sets coming from SELECT are not a slice like other dynamic programming languages. In other dynamic language, we expect the results would be a list to represent what columns you want. Moreover, In Python, we can directly fetch the database and get a dict as a result set.

In golang, we have to define the variables first, and then manually assign those variables to Scan. This implementation results in 2 problems:

  1. If we issued a SELECT *, we will encounter an error if the schema changes in the future.
  2. If we want to ask for a new column, we have to aware where uses the result sets. And modify them all carefully although they didn’t care the new column.

To sum up, we need a method to mitigate those common problems after all. My idea comes from Python, I want the result sets to be a map, so that the caller will not be impacted if any change on the database.

The solution is as follows:

Originally published on Medium

In the previous article, we had introduced how to ensure the integrity of transferring money. However, it’s not good enough to prevent going wrong. That approach will probably take time to retry many times to make things be done, so that it consumes client’s time as well as occupying the database resource.

User Scenario

Suppose there is a table for maintaining the lottery tickets, and each ticket has its own owner, serial number and a flag, remain, of whether it is used or not. In addition, there is another table to keep winning records.

For instance, A had 3 tickets but used one. And then, B got one ticket. Besides, the used ticket wins 100 dollars for A.

Simple Design

According the our previous introduction, we will design the remain flag in an unsigned integer with the default value: 1. When consuming this ticket, we uses the atomic update to increase the remain by -1. If a user has only one ticket but wants to draw twice, the second attempt will fail due to the negative value of remain.

Represent in the psudo code is like:

1
2
3
4
5
6
7
`START TRANSACTION`  
A_ticket = `select id from tickets where \
remain = 1 and user = 'A' order by id limit 1`
`update tickets set remain = remain - 1 where id = A_ticket`
reward = bingo()
`insert into rewards (id, reward) values (${A_ticket}, ${reward})`
`COMMIT`

First, A’s first ticket will be picked out, and the lottery will start after deduction. Finally, the result will be updated into the database.

But the problem occurs in the assumption that A has two tickets like the example at the beginning of this article, which means that A can draw twice at the same time. When A draws twice simultaneously, it should be successful without fail.

Run the process once with the implementation we mentioned. The first one who comes in will get a ticket number 3 and the second one who comes in will also get the same ticket. As a result, the first draw will succeed but the second attempt will be failed. If it fails, then it must try again, and then it will get the ticket number 4 and succeed.

Such a procedure would be correct after all, but it is not good enough. When you have n tickets, you obviously can draw n times at the same time within O(1). However, such an implementation leads to O(n). Among them, O(n-1) is a retry. And, the higher the n, the worse the user experience.

Random select

Here comes the second design, we don’t order by the id; instead, we pick the ticket randomly to mitigate the failure. Therefore, we modify the original select statement to become:

A_ticket = *select id from tickets where remain = 1 and user = ‘A’ order by RAND() limit 1*

In this example, we don’t care about the drawback of order by RAND(); nevertheless, the random means it will sometimes go wrong. Worst of all, it may choose the ticket with a long validity period instead of the ticket is about to expire.

Requirement

Looking back at our needs, we need to draw a lottery “simultaneously” several times when the user has a few tickets, and use the tickets in order.

Final Solution

To achieve our goal, we can leverage Redis’ help. The purpose of Redis is to have a congestion control. In order to make all clients can succeed once, we have to dispatch the tickets to them correctly while highly concurrency occurs.

Bad approach

Usually, Redis is used to be a distributed optimistic lock to make sure the permission of a critical section, e.g.,

1
2
3
4
5
6
ret = `setnx lock 1`  
if not ret:
raise Locked("already locked by others")
else:
do_something()
`del lock`

If an application can set a lock in Redis, which means he can dominate this time period until he finishes his job; on the other hand, everyone must wait for him. Solving the problem in this way is still not very helpful, at most it can only speed up the client’s retry, because the client doesn’t need to go through the database operation and the lottery procedures.

Good approach

To solve this problem, we want to support multi-entrance by using Redis. The process is to push all the current ticket numbers through LPUSH, and use RPOP to take out the first one.

Because Redis’ single command is atomic, you can ensure that the tickets are correctly allocated to each call that comes in at the same time.

Let’s take a simple psudο code as an example:

1
2
3
4
5
6
do:  
A_owned_tickets = `select id from tickets where \
remain = 1 and user = 'A' order by id`
`LPUSH A_owned_tickets ${owned_tickets}`
A_ticket = `RPOP A_owned_tickets`
while A_ticket != nil
1
2
3
4
5
6
`START TRANSACTION`  
`update tickets set remain = remain - 1 where id = A_ticket`
reward = bingo()
`insert into rewards (id, reward) values (${A_ticket}, ${reward})`
`COMMIT`
`DEL A_owned_tickets`

In the above example, we use uppercase to indicate Redis’ instructions. Let’s use the sequential diagram to get a deeper look at what’s going on.

The above is an ideal situation. Although A1 and A2 occur simultaneously, A1 is slightly earlier than A2, so there will be two complete sequences in Redis, [4, 3, 4, 3]. If A1 and A2 occur almost at the same time, it will be the following situation:

From the above figure, even if A1 and A2 occur almost simultaneously, it will not affect the accuracy. So what happens if A1 finishes its job faster, COMMIT the result and deletes the list?

We can see A2 got a nil at the first time, so he therefore had to retrieve the tickets from MySQL and push again to proceed the process. Even so, it is more efficient than the original commit failure after all the processes are run.

The worst case will happen when there is high concurrency, some clients are very fast and some clients are very slow, which will cause several retries and still fail. The flow as shown below.

If this is the case, you can adopt an alternative approach. Do not actively DEL the list, but use EXPIRE to delete. Choosing a reasonable timeout will relieve symptoms. However, it comes another problem, the validity of expired data. This is a trade-off, you can take the approach that suits your user scenario.

At last, if the amount of coming requests is greater than the owned tickets, the original schema design still works to reject the invalid operations(remain cannot be negative).

Originally published on Medium

How to avoid the race condition and the negative value

Scenario

Suppose there is a table, bank.

Both A and B have 100, and A will transfer 70 dollars to B twice.

Simple Design: Wire after Checking Balance

It would be like this in pseudo-code:

1
2
3
4
5
A_owned = `select money from bank where name = A`  
B_owned = `select money from bank where name = B`
if A_owned >= 70:
`update bank set money = A_owned - 70 where name = A`
`update bank set money = B_owned + 70 where name = B`

This is the simplest design; however, there are two defects:

  1. Remained money is incorrect after updating.
  2. After remitting, the balance comes to a negative value.

Let’s watch these two issues separately.

The root cause of the first issue is that if A initiates two transfers at the same time, both of the two times A_owned would be 100. We introduce this situation in time-series diagram below:

Finally, the A’s balance will become 30. This is the classic race condition. How to solve it?

Solution of Race Condition

In order to solve the race condition, there are three ways:

  1. Atomic Update
  2. Transaction plus Lock
  3. Version Scheme

Atomic Update

The problem of this situation is using the unreliable data. To prevent this from happening, we shouldn’t update records via using the previous results but the current value in the database.

Change the origin statement:

update bank set money = A_owned — 70 where name = A

to the new statement:

update bank set money = money — 70 where name = A

This is the atomic update.

Transaction plus Lock

Before the introduction, I have to say, most people have a misunderstanding of the transaction. The transaction is not invincible and cannot avoid all race conditions. Thinking the transaction as a panacea is so wrong.

The transaction is invincible IF the isolation level of MySQL is set to Serializable Isolation; nevertheless, Serializable Isolation affects the database performance dramatically. Therefore, the most accepted compromise is Repeatable Read Isolation. On the other hand, MySQL is not able to prevent the race condition in the such criteria. According to the reference, InnoDB cannot handle Lost Update and Phantom, i.e., the race condition will still be happened.

If you say that, what is the use of the transaction?

The transaction is to protect the integrity of a bunch of database operations. We can rollback all the changing data if there is any error occurred, so that the dirty data will not commit into the database.

Let’s get down to the business; how to leverage the transaction? The example is as follows:

1
2
3
4
5
start transaction  
A_owned = `select money from bank where name = A for update`
if A_owned >= 70:
`update bank set money = A_owned - 70 where name = A`
commit

By using for update, it can acquire an exclusive lock on the desired row. If another transaction with the same row entered, it must wait for the lock released. The working flow is like below:

From the diagram above, we can know A2 gets the value after A1 has been finished. In addition, A2 gets 30 which is not enough to the transfer, thus, A2 finishes its job without doing anything.

Version Scheme

This approach is much more complicated than the two mentioned. We have to alter table to add a new column, version, to record the changes.

Before updating data, we have to retrieve the version first then update the row on the specific version. The code is like:

1
2
3
A_owned, old_ver = `select money, version from bank where name = A`  
if A_owned >= 70:
`update bank set money = 30, version = version + 1 where name = A and version = old_ver`

Updating data and comparing the version must be executed simultaneously; otherwise, the operation will be failed. Let’s explain with a time-series diagram:

After A1 updates the row, the database returns 1 means there is one row affected, i.e., it updated successfully. But, A2 gets 0; in other words, no row is updated.

To Sum Up

We know there are three approaches to solve the race condition from the above introduction. However, the cost of the version scheme is too high, and the performance of the transaction plus lock is too poor. We really don’t need the heavy synchronization like the exclusive lock on an essential operation.

The atomic update is a better solution to apply to most use cases. Although we will no longer lose updates, negative values may still occur. Because the money is deducted 70 twice, the money is turned into -40 which is not allowed.

Total Solution

In addition to use the atomic update to modify the money, we still need some tricks to refrain from the negative values. My personal recommendation is to define the column, money, as an unsigned integer. Besides, use the transaction to protect the transfer. The final solution of this article is:

1
2
3
4
5
6
`start transaction`  
A_owned = `select money from bank where name = A`
if A_owned >= 70:
`update bank set money = money - 70 where name = A`
`update bank set money = money + 70 where name = B`
`commit`

Change two updates to the atomic update, and use transaction to guard the whole operation. Even if if A_owned >= 70 cannot stop A2, A2 will still get the failure due to the unsigned column. The transaction will be failed, and B won’t receive the money does not belong to him.

Originally published on Medium

問題描述

設計一個能夠自動補完的後端系統,就像Google或亞馬遜

設計目的

像往常一樣,我們首先須要讓設計目標更加明確。花兩分鐘問自己這些問題:

  • 這個系統需要處理多大的查詢數量(以秒計)?
  • 期待的延遲?
  • 這些查詢的文本是無邊界的嗎(像是Google)?或是有一些事先定義好的種類(像是亞馬遜的商品建議)?
  • 我們想要怎樣顯示這些補全的建議?
  • 其他

這些問題的答案會引導整個設計的選擇,例如:

  • 如果這個系統是為了一個熱門網站設計的,那麼查詢次數很可能每秒超過100K。這在現實生活中已經是非常大的量了,這個數字告訴我們兩件事:
  1. 這個系統要能夠擴容到支援100K次的查詢,這不是單一台機器能夠承受的量。
  2. 系統需要消化這100K的查詢以便更新整個自動補全的結果,這通常可以透過非同步的方式處理。
  • 自動補全系統通常對於延遲很敏感,也就是說我們期待他的P95延遲在10ms。這個數字表示我們不必要把所有的資料的快取在記憶體,無論是跨資料中心的網路呼叫或使用SSD的隨機存取都是幾個ms的等級。但是,我們應該盡量多使用記憶體來減少延遲和資料庫的負載。一個小提醒,如果使用的是關係型資料庫例如MySQL,那必須要使用足夠好的機器等級,以AWS RDS來說,需要使用db.r4.8xlarge並且搭配幾個讀取複本來處理每秒100K的查詢。
  • 假設我們要處理的是純文本輸入像是Google的自動補完,那表示字彙量很可能會很大。
  • 如果當使用者輸入1到M個字元而我們要顯示最熱門的N個推薦,那麼N和M的值會決定整個設計走向。舉例來說,當N = M = 5,如果我們決定預先計算所有推薦,那我們只需要使用(26 + 262 + 263 + 264 + 265) * 5 * avg_len_of_word的空間,約小於0.5GB,這樣的使用量可以輕鬆被保存在記憶體中。另一方面,當N = M = 10,這就需要超過1K TB的空間,這也就是說我們需要很多機器來將這些資料保存在記憶體。

作法

其中一種可行的架構看起來會像:

K-V pairs

因為有延遲限制的前提,所以動態產生推薦清單不太實際。我們需要預先計算結果,並將其用鍵值對的方式儲存起來;鍵是使用者輸入的前綴,而值是最熱門的推薦清單。例如:("mic", ["michael jordan", "microsoft"]),這表示當前綴是mic的時候,有兩個最熱門的推薦:michael jordan和microsoft。選用鍵值對而不是其他更省空間的作法,如Trie的理由是,鍵值對更方便保存/利用。

Search log

搜尋記錄保留使用者產生的原始資料,這些資料來源是可信任的,之後會被用來計算鍵值對。用上述的例子,如果使用者輸入mic,並且使用者最後要的結果是microsoft,這表示另一個使用者輸入mic也有可能是想要microsoft,這背後的演算法是*貝葉斯推斷(Bayesian inference)*。

Offline Job

我們需要一個背景任務來將搜尋記錄轉換成鍵值對,這通常是運行一個map-reduce形式的批次處理任務。接著我們將產生的鍵值對推送到一個分散式存儲,例如:Redis或HBase;我們也需要將鍵值對推送到前方的伺服器來更新他們的快取。實務上,我們可以透過MQ如Kafka來可靠的推送結果。我們需要直接更新伺服器的理由是,這樣可以減少熱門資量在資料庫上的負載。

Servers

這些伺服器負責快取那些預先計算的結果和處理所有來自使用者的查詢。當輸入的字段沒有命中自己的本地快取,伺服器就查詢後端的分散式鍵值存儲。如果資料量超過一台機器的記憶體所能承受,我們就需要透過雜湊的方式將那些前綴分片在不同機器上,並且,為了能兼顧擴容,我們需要像下圖般維護複本;這個設計很常見也與其他設計問題相似。你可以在我其他文章內找到更多關於分片/複本的介紹。

在上圖中,我們使用第一個字母來做分片(這可能導致分片不平衡)。我們儲存所有首字母是a的鍵值對在節點一和節點二,首字母是b的放在節點二和三,而首字母是c的則是放在節點一和三,並且我們將這些資訊放在一個註冊表中。實務上,這個註冊表可以放在支援Paxos-like協定的任何伺服器群集(包含ZabRaft)。路由層隱藏所有資料分片的複雜度;首先讀取/快取註冊表,接著透過輪詢的方式發送請求到對應節點,例如:將a開頭的搜尋關鍵字依序送到節點一和二。

個人化資料

在這個架構中,我們並沒有處理個人化資料。如果使用者在Google中搜尋,搜尋結果可能會參考個人興趣,這可以透過結合過去的搜尋歷史來產生。

CDN

另一個設計選擇是,我們通常會導入CDN來減少延遲。我在這篇文章先忽略這個討論,你可以參考另一篇文章來了解為什麼需要CDN。

作者:KKXX
文章出處:https://medium.com/@morefree7/design-the-auto-complete-type-ahead-backend-dc7ec56dc1df
譯者:我

Originally published on Medium

問題描述

設計一個可以追蹤在文章/動態/照片的讚數系統,且能夠擴容

如何達成

這是一個有趣的問題,一開始看起來很簡單,但是實際上需要小心思考才能夠做對。我會在下面解釋我的想法,然後展示如何在需求改變時演進架構。

最簡單的設計我們可以看到像這樣:

下面是這個簡單架構的優缺點:

  • 保證資料一致性。任何在寫入之後的讀取都保證能夠拿到最新的讚數,使用者不會看到鎖死的數值。
  • 這無法很好的擴容。當數以百萬計的同時寫入會拖慢資料庫的效能。雖然我們可以用post_id來分shard,但是這無法解決大量的同時寫入。
  • 這在即時計算數量上有著高昂成本(注意,Innodb沒有維護一個內部計數,相反的,當下了SELECT COUNT(*) FROM LIKES WHERE...時幾乎總是重掃整張資料表)。我們也許可以透過別的組件,例如:快取或別張資料表來儲存計數,但即便我們這麼做,還是有一個重要的問題需要克服:如何確保實際的讚和額外儲存的讚數是一致的。

為了解決剛提到的兩個缺點,我們必須稍微犧牲一點資料一致性:在我們按下讚之後,後端立即回覆而不需要等這筆紀錄寫進資料庫。我們也會在下面介紹一些額外的組件:

  • MQ:一個訊息佇列用來充當非同步更新的緩衝,此外也能可靠地將(post_id, user_id)廣播給多個訊息消費者。Apache Kafka是在業界很受歡迎的選擇。
  • Updater:這是一個訊息消費者,負責將資料插入點讚資料表。
  • Batch updater:透過降低資料一致性,我們可以將一樣的post_id做批次處理來降低資料庫寫入的負載。麻煩的是,必須要要引入額外的基建來確保批次更新是可靠的,例如一些串流處理集群,如:Apache FlinkApache Storm
  • Cache:負責儲存最新的讚數。

這第二個設計的優缺點如下:

  • 透過使用MQ做為緩衝,可以降低資料庫的寫入負載。
  • 透過使用batch updater,可以進一步降低熱門文章的寫入負載。
  • 點讚表和讚數表可能存在資料不一致。即使沒有任何組件失效也有可能發生,畢竟大部分的MQ無法保證訊息只被處理一次;我們可能會看到比實際更多的讚數。

為了解決資料不一致,我們可以採取的方案是週期性的從點讚表重算讚數。因為點讚表總是有最原始的資料,重算的結果會更可靠。為了從文章得到即時的讚數,重算的結果必須和線上的結果合併,如下圖展示的:

如上圖所示,我們每天定時在12:00 AM從點讚表計算讚數。另一方面,在Real time events資料表中應該保留從12:00 AM到當下的讚數。透過讀取兩張資料表,我們應該可以得到相當精準且即時的讚數。這個設計參考了Lambda Architecture

作者:KKXX
文章出處:https://medium.com/@morefree7/design-a-system-that-tracks-the-number-of-likes-ea69fdb41cf2
譯者:我

後續回應

From Timofey Asyrkin

在這個情境下,我們真的在乎重複嗎?這很有趣,來看我們有甚麼選項:

  1. 當使用者按下讚,我們真的要立刻更新前端而不知會後端嗎(因為前後端非同步)?如果這樣做,當立刻刷新頁面會發生甚麼事?很大的機會我們會沒看到我們自己的讚,然後重新讚一次,這樣就會導致重複。
  2. 我們不需要前端馬上更新讚數,當資料送達到後端,後端發送通知給前端。這樣做的問題是,如果使用者沒有馬上看到更新,在沒有驗證機制下他們很有可能重新讚一次,這個新的要求還是會送進系統。
  3. 我們同步更新快取後將要求送進MQ來更新主要的儲存結構。如果使用者重新刷新頁面,我們從主要儲存和快取(如果有存在的話)拿資料。這麼做的問題在於有可能在主要儲存上會有錯誤。

譯者話:關於第三點我覺得應該是正解,但為什麼會說有可能發生問題呢?因為無論有沒有快取,都有可能在主要儲存上發生問題。我猜應該是指當主要儲存失效時,會有資料不一致吧,但如果在MQ前還卡一關快取,應該在做資料校對的時候會有更多可以參考的資訊才對。

Originally published on Medium

在這個病毒肆虐的時間點找工作,有個很大的優勢:全部都是『線上』面試。省去了舟車勞頓的辛苦,時間上也更容易安排,以往面試兩小時可能請假就要直接請個半天,但在居家辦公時期,面試兩小時就只需要請兩小時;甚至可以直接約中午,連假都不用請。

在開始寫這陣子的面試經驗前我先簡單介紹一下背景

6+year嵌入式系統開發經驗(C/C++)

4+year後端開發經驗(Python/golang)

1+year前端開發經驗(Node.js/Vue.js)

英文聽說讀寫略懂略懂

基本上,沒刷題,沒什麼準備,考古題/常見題一個也沒看過,面試邀約來了就上。想要找後端相關的工作,內心當中對於offer的排序是:地點、薪資、工作內容。邀約通常來自獵頭,也有些來自HR直接邀約。

這次認真面試花了兩個月,但從開始面試到確定offer,大概用了半年。總共談了十間公司。接下來按照時間序一個一個紀錄,主要是面試流程和內容,有些因為時間很久了,印象會有點模糊,就略微帶過。至於工作內容之類的,那個不是重點,頂多約略描述。

1Binance(幣安),這間是我主動投的。線上面試。那時候數位貨幣水漲船高,我也想進幣圈試試。但幣安的後端開發主要是JAVA,我沒有相關經驗,所以投了DevOps工程師。

第一關:著重在工作經歷的部分,會對於曾經做過的專案項目進行深入探討,同時也會簡單介紹一下公司,時間大概1–1.5小時。

第二關:著重在考核DevOps相關的經歷,例如VPC/子網怎麼規劃、路由不通怎麼故障排除、內部/外部DNS規劃。諸如此類的問題,我其實也沒有相關經驗,回答的都很勉強,時間大概1–1.5小時。

結果:掰

2Appier,此時的Appiear要在日本IPO。現場面試。我忘了是我自己投的還是獵頭介紹,總之是面試後端開發的職缺。

第一關:同樣會深入探討工作經歷,包含做過的專案/產品,背後的架構和資料流等,時間大概1–1.5小時。

第二關:白板題,會出一個情境案例,要根據需求設計一個系統架構。題目是:在客戶端服務上有一個SDK,會把客戶端的使用狀態回報給這個新系統,數據類型是一個JSON,而數據量是海量,而這個新系統要負責從這無數JSON中做一份統計報表。

這個題目我個人覺得很有趣,畢竟這種考題切合現實,不是像leetcode那種實務上不太會碰到為了考試而考試的題目。但我這關沒有答的很好,當下有點壓力,不過,考官不會施壓,會在旁邊給些提示/建議,讓這個系統能夠成型。時間大概1–1.5小時。

結果:掰

如果對於題目有興趣,歡迎私訊我,我們也可以一起研討一下,我心裡應該是有個解答了,不過還是可以討論交流看有沒有缺漏。

上面兩次面試發生的時間很早,在2021年初,距離第三次面試中間間隔兩個月。為什麼空白了這麼久,最大的原因是經過了前兩次面試,我發現我有點廢啊!基本上答題都不甚理想,幣安就算了,畢竟幾乎都在考operation,這我本來就沒什麼經驗,但Appier的系統架構設計還答成那樣,我覺得實在慘,所以花了點時間把一些系統架構的書/網站/資源K一下。覺得心態平復了,才繼續開始面試。

3KK Stream,此時的KK Stream看到Appier的成功也正醞釀要日本IPO。這機會是透過獵頭介紹的後端開發。現場面試

第一關:Leetcode,考五題三小時,第一題是git相關的選擇題十題,有git經驗的都可以簡單回答(雖然我錯一個);後面四題是程式題,難度大概介在easy+~medium,我這種沒刷題的小白也可以輕鬆寫完。

第二關:技術面試,著重在過往的經驗,會對經手的項目作深入探討,必要的時候需要寫白板,然後,會介紹公司和工作內容。整體面試氣氛輕鬆愉快沒有壓力,時間約2小時。

第三關:HR電話面試,主要是人格/性向之類的訪談,還有介紹薪資福利等。

第四關:BU主管,也是純聊天,談談生涯規劃,聊聊過往經歷,時間約1小時。

整個面試的過程我覺得是十場中最愉快的,就真的是在聊聊中結束。但不得不說,KK的面試流程和耗時也是十場最久的。從開始leetcode到package出來,超過一個月。

結果:offer got

4AMIS,MaiCoin的姐妹公司,也是獵頭介紹後端開發。我還是沒放棄進入幣圈。線上面試。先是介紹AMIS的產品和工作內容,然後就我過去的工作經驗做些探討,只能說,就我感覺面試者不太友善,有點不知道哪來的優越感。

結果:掰

5Innova Solution,外商醫療支付平台,對方HR主動聯繫。電話面試,後端開發。一開始是HR主動跟我聯絡,因為是純英文工作環境,所以測試一下我的英文能力,就用電話英文面試了一下。雖然沒做過準備,但英文自我介紹我覺得還中規中矩,就是想到什麼說什麼,然後HR簡略介紹一下公司,並且邀約下一次面試。但我之後拒絕了,為什麼呢?我大概有幾個考量,第一Innova Solution沒有自己的品牌,他其實是另外一間公司的研發(外包)中心,所以醫療支付其實是另一間公司的產品;其次是,醫療領域的步調略慢,產業別大概介在傳產和科技業之間,有點像是銀行的感覺。

綜合考量下,最後我婉拒面試。

6遊戲橘子,獵頭介紹,架構師。線上面試。從後端切架構師其實這跳有點多,技能樹不太一樣,所以當獵頭推薦我這個機會的時候,我還特別請獵頭去詢問我沒有網路安全的相關經歷,這樣足夠嗎?後來回覆是:可以,所以就接下邀約,過程平和,相談甚歡,但整體面試過程中發現,我的能力其實不足以勝任他們要的這個角色,這算是我的自知之明。最後也證實我的想法是對的。

有個問題其實我到現在還沒有答案,『微服務vs.單體架構,如果每個module/package都可以單獨部署,例如JAVA的.jar或Python的pip,甚至CentOS的RPM,這樣微服務還有什麼優勢?』我當下回答的有點鳥:『微服務的優勢在於損害控管,每個服務獨立上線可以快速釐清問題的死點在哪個服務上』,但直到現在我也不知道怎樣的答案算是標準答案。

結果:掰

7Berry AI,獵頭介紹,全端開發。線上面試。Berry AI是台灣上市公司飛捷科技的子公司,是一間不像新創的新創,有燒不完的資金,所以可以慢慢找產品方向。直到本文撰寫完成之際,都還沒有能獲利的產品推出。在開始面試之前,有一次簡單和CTO的線上會談,介紹公司狀況、產品規劃等,確定有意願,才會開始正式的面試流程。

第一關:CTO,有一份考題,會採用共編的方式作答,有點類似現場面試的白板題,題目涵蓋OS/Python/C語言,OS的部分偏重multi-thread/multi-process,有一題滿有趣的,如何用multi-thread做排序。Python就基本題,decorator的用法和shallow copy的問題。C語言則是call-by-value和指標的運用。整體來說,難度沒有很高,放寬心作答即可。

第二關:CEO,介紹公司營運狀態和公司歷史,然後做些人格和性向的訪談,包含各時期的轉職心境和規劃。主要是純聊天,態度和善沒有架子。

第三關:PM,剛剛沒問到的,想知道的秘辛,都可以在這裡發問,如果沒什麼問題,整個流程就結束了。

總耗時約3小時。

結果:offer got

8浪Live,HR邀約。架構師。線上面試。我直到這時候才知道浪Live其實是台灣上市公司,而且除了直播,還有一堆產品。一開始由HR主動聯絡我,並且花了不少時間介紹公司狀況和福利等,除此之外也對之後的職務做了摘要。在接觸這麼多獵頭和公司HR之後,我覺得這個HR的認真程度絕對可以排前幾名。也因此,我答應他們的面試邀請。

第一關:台灣區的RD頭,就我個人的經歷和經驗做些了解,主要是討論技術問題,包含當初的技術選型原因等。沒有壓力,過程平順。

第二關:台灣區的HR頭,做些人格和性向的訪談,也是純聊天。

總耗時約2–2.5小時。整個面試的過程讓我有點意外,畢竟,沒有考試,沒有白板題,幾乎都是聊天,這應該是所有面試中整個最沒壓力的一場。另外,雖然是架構師,但其實還是要經手RD相關的事務。

結果:offer got

9Hardcore Tech,或稱GoFreight,獵頭介紹。後端開發。線上面試。穩定獲利的新創,產品主要是物流平台的整合。這間的面試是我覺得十場中最硬核的,難怪叫做Hardcore Tech。

第一關:HackerRank,線上刷題。兩小時要寫五題,難度約是medium+,對我這種小白說來,痛苦到一個不行。五題只寫完三題,但還是拿到門票了。

第二關:面試官出兩題白板題,一樣採共編的方法,這兩題白板題應該也是medium+難度的。上一關的HackerRand就已經要死了,這次白板題直接死去,根本寫不出來,面試官一定覺得我是去鬧的。

第三關:還是白板題,但這白板題的難度沒有很高,主要是給你一個情境,讓你的程式必須不斷應付新進來的需求,要加參數,寫新function,做design pattern。這算是實務題,本來就是我擅長的,所以答起來還算順暢。

結果:無聲卡

10Linc,獵頭介紹,號稱純正矽谷直營台灣分部。全端team lead。線上面試。這也是一間開始穩定獲利的新創,聽獵頭說薪水是純正矽谷等級,做的是電商的後台,例如出貨進度、訊息通知、消費分析、商品推薦。因為是電商後台,所以要整合許多第三方的系統,例如各家不同的物流。

第一關:也是先要線上考,但有別於leetcode或HackerRank,這間公司選擇自己出題。題目很生活化也很實用,我覺得這種題目才有考的價值。

第二關:台灣區的RD頭,純英文的技術問答。因為是純外商,所以是全英文工作環境,不免俗就是要測試英文能力。所以技術問題難度不高,有基本的computer science能力都可以回答,主要是英文口說要流暢。然後也有白板題,難度是easy,就是經典的大數相加。最後就是介紹公司狀況,然後部門組成,人選要求,工作內容等。

第三關:台灣區的PM,因為team lead和PM會經常合作,所以這邊是彼此互相了解。也有幾個情境題,例如兩個月的時間,要如何讓一個全新的系統上線。透過這種方式來了解帶team的方式和經驗。

第四關:台灣區的另一個team lead。白板題,難度是medium+,也是痛苦到不行,最後沒答完就超時。

結果:無聲卡

這次面試耗時巨久,產業各異,甚至連位置都不一樣,有後端、全端、team lead,甚至架構師。面試到後面其實身心俱疲,而戰績也不怎麼理想:3/10,不過也有了滿多特別的經驗,例如英文面試和各種白板題。在居家辦公時期,寫白板題的經驗格外有趣,大部分公司都直接用google doc充當白板就直接在上面寫了。

我對面試的想法是接觸新的人/事/物以此來認識自己的不足,而事實上我也的確在這過程當中心態爆炸了,覺得我怎麼這麼廢。但畢竟還年輕,所以也算是把握機會長長見識,感謝這些花費時間心力幫助我成長的面試官們。

也許有人會問,『你都不刷題,還面什麼試啊?』,我這邊說說我的想法。程式碼是溝通交流的工具,好的程式碼可以讓後續接手的人能輕易上手,「重構:改善既有代碼的設計」這本書說,好的程式碼就是文件。在資訊技術這麼發達的現今,已經不是以前對CPU/memory錙銖必較的時代了,寫程式的人要做到的不是為了榨出額外那一咪咪的效能而寫程式,而是為了更好的合作和交流。不要誤會,我並不是說刷題不重要,我要說的是,如果兩層for-loop的O(n²)可以更好的讓人看懂,就不需要自以為是的使用各種奇耙寫法硬生出O(n)。

而刷題說穿了其實就是不斷去洗鍊你思考的流程,讓你能夠逼近那完美解,所以出了各種奇耙問題要你使出各種奇耙解法,這種為了做而做的事我不喜歡。

所以這次碰到很多有趣的白板題,例如Appier的系統設計或者Linc的實務問題,我覺得這才是面試一個RD應該出的考題,看得出來是有經過思考設計的題目,目的是為了找出他們真正需要的人。即使當下我被按在地上摩擦,但整體經驗是快樂的,而且我也知道該怎麼去補足,這才是有建設性的刷題。

此外,這次面試也合作了一些獵頭,每個人風格都不太一樣:

有些人就像房屋仲介一樣貼一堆模糊的資訊在那,然後一直要騙你的聯絡方式,例如就只寫『某知名金融企業招有經驗的後端開發』,沒薪水範圍、也沒職務內容,甚至根本沒職務要求。這種我超厭惡的,不想做就不要做。

進階一點的會做一些基本的訪談,了解你的需求,知道你的能力,甚至幫你規劃履歷(也許還會幫你加料)。

再主動一點的,甚至會直接幫你媒合,主動告訴你有些什麼樣的機會,然後詢問你的意願,給建議,並且規劃面試的流程和戰略。

做的更好的是,每次面試結束,都會詢問整個面試的經過,鉅細彌遺的紀錄,以便給下一個要apply同個職缺的人建議。並且會主動追公司的後續,並持續向你更新狀態。到核心流程的時候也會告知如何談到好的價碼,該怎麼開價才是適合的。

這次每種類型我都有碰到,其中有一個人我會很願意推薦給別人:Solution First的Kevin

Originally published on Medium

Leetcode concurrency in golang總結

這次在leetcode中選了concurrency類別的6題(總共也只有6題)來練習golang,主要是為了熟悉golang的語法,和體會goroutine的強大。實際解題後,對於golang這個語言又有了新一層的認知,在這篇我會就我寫過的程式語言做些比較並給點簡單的心得。

首先,先回顧一下這六題:

#1114,是最簡單的一題,透過channel的強大支援,就可以很好的協調所有的goroutine。

#1115,就再稍微有趣一點,依然可以用channel簡單做,但其實也可以透過Barrier收得更美,只是在Barrier底下要維持foobar的順序要花點心思。

#1116,可以透過channel做,但因為有三個goroutine,要分配誰該做事誰該等待會有點複雜;用Barrier做的話也有誰先誰後的問題要解決。所以最後我決定用一個比較不一樣的選擇:使用生產者消費者模型。

#1117,典型的信號題,透過信號可以順利解決問題,但golang的信號無法空等,和pthread的行為不同,這點要額外注意。

#1195,典型的Barrier題,為什麼這題是典型題,但1115不是?最主要的差別在於,1195的所有worker雖然全部都要按照順序執行某一樣固定任務,但每一個任務只有一個worker確切會做事,因此worker間不需要彼此協調。但1115則是需要foo和bar同步,foo一定要先,才是bar。

#1226,典型的教科書題,哲學家用餐問題。一堆教科書都寫過解法所以也沒什麼好說的,這邊就是其中一種教科書解法,依序拿兩個鎖,反序釋放。

練習了這些同步的問題,對於goroutine和channel有了進一步認識,但這些問題其實沒帶到兩個進階用法:buffered channel和select(unblock read/write),之後我會再找機會練習;另外有些進階語法也沒機會使用到,例如把channel當初func的input/output。這些其實在看open source的時候都還算常見,但因為不熟所以會看得很痛苦,這次沒練習到實屬可惜。

最後是與一些常見語言的比較,這邊舉Linux C、Python3和golang做對比。

語感上Python3還是完勝,主要是因為Python是個弱型別語言,所以變數和容器都可以龍蛇雜處,另外,Python的list/dict也不需要allocate,golang必須要在宣告後make,這點在純Python programmer眼裡實在有夠不自然。但golang還是比C語言好上一截,至少在array/slice的操作上,比起C更加直覺,天生支援回傳多值也比C方便許多。此外,golang的strcut還可以包function,這點也比C更加實用。

golang有一點很特別的是,支援指標/定址,這點褒貶不一,但在我眼裡其實弊大於利。首先,在宣告func和struct時,實在很難搞懂到底要不要加星號,舉例來說,sync.WaitGroup大部分的範例都有加星號,但sync.Mutex卻沒有,這標準是什麼?我現在就也是照抄而已,還沒搞懂背後的原因。至於func的參數,golang天生就是call-by-value,即使是array/slice/map也是call-by-value,這很容易讓無論是C還是Python programmer踩坑。

在multi-task方面,Python可以選擇multi-process/multi-thread/coroutine,golang有天生自帶的goroutine,而C只有multi-process/multi-thread。不得不說,goroutine實在太強太好寫了,Python的coroutine還要搞一堆async/await,麻煩到不行,也有夠不直覺。但goroutine不用,就正常寫就好,在這點絕對是大勝Python十條街以上。而且在同步處理的部分,有channel實在方便,可以通知/等待,在C/Python都必須透過pthread提供的lock和condition來協調,其實不太好用,沒有語法糖。

總體來說,我個人還是覺得Python > golang > C,但在同步處理上或者單一功能的小型app上,golang > Python > C。主要原因有二,其一是goroutine很容易開發;其二,golang可以直接build出binary比起肥死人的Python和一堆side-packages更容易移植。

Originally published on Medium

Leetcode concurrency #1226 The Dining Philosophers (in golang)

這是concurrency的最後一篇,前面已經試過幾種常見的同步模式,這次要解的題目是經典的哲學家問題,但處理的手法應該已經被很多教科書討論過了,不外乎引入服務生、搶到叉子就吃搶不到就等,和比較近期的用餐券換叉子。

這麼經典的問題教科書應該都有寫過我就大略描述下就好。總之,五個哲學家坐在同一張圓桌,每個人都有一碗吃不完的麵,但只有五支叉子放在人與人之間,要吃麵就一定要用兩支叉子,沒有兩支叉子就發呆。這裡有個變體,要接收一個整數,代表每個人至少要吃過幾次,例如:n=1,表示每個人都至少吃過一次就散會。

解法也很簡單,我選用最單純的搶叉子,每支叉子按順時鐘順序編號,每個人都要先拿編號小的才能拿編號大的,但放叉子要從編號大的先放,才能放小的。

每個叉子都是一個互斥鎖,小的先鎖再鎖大的,大的先解鎖才解小的。唯一要注意的一點是,吃完以後要看一下是不是每個人都吃過了,如果大家都吃過就準備結束散會;一定要在吃完的時候就看,因為這時候才是在critical section內,過了就要再搶一次,那沒必要。

有趣的一點是,我在Leetcode的playground跑這段code,會發現有些哲學家餓到不行,會狂搶叉子,導致結束的時候某人大概續了幾千次麵,真是個大胃王。

Originally published on Medium

Leetcode concurrency #1195 Fizz Buzz Multithreaded (in golang)

從開始刷concurrency到現在已經接觸過,Event、Lock和Semaphore,這題會用到另一個同步處理也會使用的技術:Barrier,Barrier是指當所有做事的人都必須將queue內的event全部依序執行,例如:ABC三個人同時拿queue[0]做,等大家都做完後,三人繼續拿queue[1],依此類推。

先看一下題目,看完就會知道為什麼一看就知道要用Barrier。有一個class有四個method(又來了),必須將四個method放在四個thread上執行,這四個method分別是:在3的倍數時印fizz、在5的倍數時印buzz、在15的倍數時印fizzbuzz,最後是上面都沒中就印數字。

因為golang沒有實作自己的Barrier,我本來想用WaitGroup來做,但怎麼試怎麼怪,最後決定WaitGroup還是維持他單純的用途就好:等所有人做完。那麼,就需要實作一個Barrier,這邊可以使用sync.Cond。如果寫過linux C應該會對pthread_cond_t有印象,這兩個是等價的。

當有了Barrier的實作,這題就一點也不難,就是分別判斷能不能整除3或5來決定個別要做甚麼,把輸入的整數丟進去大家一起一個一個做就好。

Originally published on Medium

0%