grpc原始碼分析1-context
io.grpc.Context
表示上下文,用來在一次grpc請求鏈路中傳遞使用者登入資訊、tracing資訊等。
Context本身是Immutable的,但是它儲存的狀態不一定是。
Context中儲存資料使用的是KV對映。
Key定義如下, 通過這種方式,而不是使用String可以有多個相同name的不同Key,而name則只用來做debug資訊。
public static final class Key<T>{ private final String name; private final T defaultValue; }
獲取Key對應的value即從Context中找到對應的對映,不過Key也提供了get這個簡便方法,從當前context獲取對應的value
/** * Get the value from the {@link#current()} context for this key. */ @SuppressWarnings("unchecked") public T get() { return get(Context.current()); } /** * Get the value from the specified context for this key. */ @SuppressWarnings("unchecked") public T get(Context context) { T value = (T) context.lookup(this); return value == null ? defaultValue : value; }
那麼Context中是如果儲存這個KV對映的呢, 我們通常會使用HashMap來進行儲存。
不過HashMap是一個比較全能的Map實現,有對put、delete、get等操作都有優化,因此內部有很多輔助結構,這就增加了記憶體消耗。(HashMap實現分析可以參考我的另一篇文章)
在Context的使用場景中,只需要put和get操作。
例如在當前Context基礎上,增加一些KV對,是put操作;查詢當前某個key對應的value,是get操作。
最開始的版本實現裡,Context使用的是二維陣列Object[][],第一維是Key,第二維是Value。
這樣的缺點是查詢的時間複雜度是線性的(其實大多數場景的key的數量都比較少)。
所以在HashMap和陣列間的時間複雜度和佔用空間的權衡就是Hash Array Mapped Trie 資料結構了。
在grpc中的實現是io.grpc.PersistentHashArrayMappedTrie
。
下面使用SizeOf
工具簡單通過deepSize對比一下HashMap
和PersistentHashArrayMappedTrie
的記憶體佔用情況。
private static final Context.Key<String> userName = Context.key("userName"); private static final Map<Context.Key<?>, Object> cache = new HashMap<>(); private static PersistentHashArrayMappedTrie<Context.Key<?>, Object> persistentHashArrayMappedTrie = new PersistentHashArrayMappedTrie<>(); public static void main(String[] args) { cache.put(userName, "hello"); persistentHashArrayMappedTrie = persistentHashArrayMappedTrie.put(userName, "hello"); SizeOf sizeOf = SizeOf.newInstance(); System.out.println(sizeOf.deepSizeOf(cache)); System.out.println(sizeOf.deepSizeOf(persistentHashArrayMappedTrie)); }
可以看到記憶體佔用差別還是比較大的,因為HashMap裡面的Node儲存了key、hash、value、next等欄位,並且預設使用一定大小的capacity數量作為起始capacity來避免反覆的resize。
Context常用用法如下。首先獲取當前context,這個一般是作為引數傳過來的,或通過current()獲取當前的已有context。
然後通過attach方法,繫結到當前執行緒上,並且返回當前執行緒
Context current = xxx. Context previous = current.attach(); try { // do something in context } finally { current.detach(previous); }
Context的主要方法如下
- attach() attach Context自己,從而進入到一個新的scope中,新的scope以此Context例項作為current,並且返回之前的current context
- detach(Context toDetach) attach()方法的反向方法,退出當前Context並且detach到toDetachContext,每個attach方法要對應一個detach,所以一般通過try finally程式碼塊或wrap模板方法來使用。
- static storage() 獲取storage,Storage是用來attach和detach當前context用的。
public class Context { public static final Context ROOT = new Context(null, EMPTY_ENTRIES); public static Context current(){ Context current = storage().current(); if (current == null) { return ROOT; } return current; } private Context(PersistentHashArrayMappedTrie<Key<?>, Object> keyValueEntries,int generation){ cancellableAncestor = null; this.keyValueEntries = keyValueEntries; this.generation = generation; validateGeneration(generation); } public Context attach(){ Context prev = storage().doAttach(this); if (prev == null) { return ROOT; } return prev; } public void detach(Context toAttach){ checkNotNull(toAttach, "toAttach"); storage().detach(this, toAttach); } // For testing static Storage storage(){ Storage tmp = storage.get(); if (tmp == null) { tmp = createStorage(); } return tmp; }
上面提到的wrap模板方法如下,可以wrap一個Runnable(不需要返回值的使用)或Callable(要返回一個值)
public Runnablewrap(final Runnable r){ return new Runnable() { @Override public void run(){ Context previous = attach(); try { r.run(); } finally { detach(previous); } } }; } public <C> Callable<C> wrap(final Callable<C> c) { return new Callable<C>() { @Override public Ccall()throws Exception { Context previous = attach(); try { return c.call(); } finally { detach(previous); } } }; }
Context的attach、detach方法都呼叫了Storage對應的方法。
grpc的預設的Storage實現是使用ThreadLocal的ThreadLocalContextStorage
。ThreadLocal是實現可以參考ThreadLocal使用和原始碼分析
final class ThreadLocalContextStorageextends Context.Storage{ // ThreadLocal儲存的是當前的Context static final ThreadLocal<Context> localContext = new ThreadLocal<>(); @Override public Context doAttach(Context toAttach) { Context current = current(); localContext.set(toAttach); return current; } @Override public void detach(Context toDetach, Context toRestore) { if (current() != toDetach) { log.log(Level.SEVERE, "Context was not attached when detaching", new Throwable().fillInStackTrace()); } if (toRestore != Context.ROOT) { localContext.set(toRestore); } else { // Avoid leaking our ClassLoader via ROOT if this Thread is reused across multiple // ClassLoaders, as is common for Servlet Containers. The ThreadLocal is weakly referenced by // the Thread, but its current value is strongly referenced and only lazily collected as new // ThreadLocals are created. // // Use set(null) instead of remove() since remove() deletes the entry which is then re-created // on the next get() (because of initialValue() handling). set(null) has same performance as // set(toRestore). detach時清除到toDetach的引用,避免發生引用洩露,雖然ThreadLocal本身是Thread裡面weak reference的,但是value確是強引用的,所以要通過null要主動去掉引用 localContext.set(null); } } // 獲得當前Context,通過ThreadLocal獲取,如果ThreadLocal中沒有值,說明當前Context為root context @Override public Context current() { Context current = localContext.get(); if (current == null) { return Context.ROOT; } return current; } }
ThreadLocal線上程池場景的問題
熟悉ThreadLocal的朋友知道,ThreadLocal儲存線上程的ThreadLocalMap中,如果從一個執行緒提交一個Runnable或Callable到執行緒池,那麼
線上程池中則獲取不到提交任務的執行緒的ThreadLocal了。那麼如何解決這個問題呢。
grpc中同樣提供了一些對Executor的委託封裝。
public static Executor currentContextExecutor(final Executor e){ class CurrentContextExecutorimplements Executor{ @Override public void execute(Runnable r){ e.execute(Context.current().wrap(r)); } } return new CurrentContextExecutor(); }
這裡的Executor執行execute時,會先呼叫Context.current()獲取當前Context物件,然後使用Context.wrap(Runnable)方法wrap提交的Runnable。
public Runnablewrap(final Runnable r){ return new Runnable() { @Override public void run(){ Context previous = attach(); try { r.run(); } finally { detach(previous); } } }; }
再看一下wrap方法,由於它是Context類的方法,所以new Runnable()這個匿名內部類的attach()呼叫實際是Context.this.attach()所以這個方法
對應到上面是Context.current這個提交任務的當前Context來執行attach(),這樣就完成了Context從提交執行緒到實際執行執行緒的傳遞。
題外話,通用的ThreadLocal解決方案可以參考transimitable-thread-local ,
可以解決一些tracing框架的例如上下文tracing資訊丟失問題。