本文主要研究一下Cassandra的FailureDetector

IFailureDetector

cassandra-3.11.4/src/java/org/apache/cassandra/gms/IFailureDetector.java

public interface IFailureDetector
{/*** Failure Detector's knowledge of whether a node is up or* down.** @param ep endpoint in question.* @return true if UP and false if DOWN.*/public boolean isAlive(InetAddress ep);/*** This method is invoked by any entity wanting to interrogate the status of an endpoint.* In our case it would be the Gossiper. The Failure Detector will then calculate Phi and* deem an endpoint as suspicious or alive as explained in the Hayashibara paper.** param ep endpoint for which we interpret the inter arrival times.*/public void interpret(InetAddress ep);/*** This method is invoked by the receiver of the heartbeat. In our case it would be* the Gossiper. Gossiper inform the Failure Detector on receipt of a heartbeat. The* FailureDetector will then sample the arrival time as explained in the paper.** param ep endpoint being reported.*/public void report(InetAddress ep);/*** remove endpoint from failure detector*/public void remove(InetAddress ep);/*** force conviction of endpoint in the failure detector*/public void forceConviction(InetAddress ep);/*** Register interest for Failure Detector events.** @param listener implementation of an application provided IFailureDetectionEventListener*/public void registerFailureDetectionEventListener(IFailureDetectionEventListener listener);/*** Un-register interest for Failure Detector events.** @param listener implementation of an application provided IFailureDetectionEventListener*/public void unregisterFailureDetectionEventListener(IFailureDetectionEventListener listener);
}
复制代码
  • IFailureDetector接口定义了isAlive、interpret、report、forceConviction、registerFailureDetectionEventListener、unregisterFailureDetectionEventListener方法

FailureDetector

cassandra-3.11.4/src/java/org/apache/cassandra/gms/FailureDetector.java

/*** This FailureDetector is an implementation of the paper titled* "The Phi Accrual Failure Detector" by Hayashibara.* Check the paper and the <i>IFailureDetector</i> interface for details.*/
public class FailureDetector implements IFailureDetector, FailureDetectorMBean
{private static final Logger logger = LoggerFactory.getLogger(FailureDetector.class);public static final String MBEAN_NAME = "org.apache.cassandra.net:type=FailureDetector";private static final int SAMPLE_SIZE = 1000;protected static final long INITIAL_VALUE_NANOS = TimeUnit.NANOSECONDS.convert(getInitialValue(), TimeUnit.MILLISECONDS);private static final int DEBUG_PERCENTAGE = 80; // if the phi is larger than this percentage of the max, log a debug messageprivate static final long DEFAULT_MAX_PAUSE = 5000L * 1000000L; // 5 secondsprivate static final long MAX_LOCAL_PAUSE_IN_NANOS = getMaxLocalPause();private long lastInterpret = Clock.instance.nanoTime();private long lastPause = 0L;private static long getMaxLocalPause(){if (System.getProperty("cassandra.max_local_pause_in_ms") != null){long pause = Long.parseLong(System.getProperty("cassandra.max_local_pause_in_ms"));logger.warn("Overriding max local pause time to {}ms", pause);return pause * 1000000L;}elsereturn DEFAULT_MAX_PAUSE;}public static final IFailureDetector instance = new FailureDetector();// this is useless except to provide backwards compatibility in phi_convict_threshold,// because everyone seems pretty accustomed to the default of 8, and users who have// already tuned their phi_convict_threshold for their own environments won't need to// change.private final double PHI_FACTOR = 1.0 / Math.log(10.0); // 0.434...private final ConcurrentHashMap<InetAddress, ArrivalWindow> arrivalSamples = new ConcurrentHashMap<>();private final List<IFailureDetectionEventListener> fdEvntListeners = new CopyOnWriteArrayList<>();//......public boolean isAlive(InetAddress ep){if (ep.equals(FBUtilities.getBroadcastAddress()))return true;EndpointState epState = Gossiper.instance.getEndpointStateForEndpoint(ep);// we could assert not-null, but having isAlive fail screws a node over so badly that// it's worth being defensive here so minor bugs don't cause disproportionate// badness.  (See CASSANDRA-1463 for an example).if (epState == null)logger.error("Unknown endpoint: " + ep, new IllegalArgumentException(""));return epState != null && epState.isAlive();}public void interpret(InetAddress ep){ArrivalWindow hbWnd = arrivalSamples.get(ep);if (hbWnd == null){return;}long now = Clock.instance.nanoTime();long diff = now - lastInterpret;lastInterpret = now;if (diff > MAX_LOCAL_PAUSE_IN_NANOS){logger.warn("Not marking nodes down due to local pause of {} > {}", diff, MAX_LOCAL_PAUSE_IN_NANOS);lastPause = now;return;}if (Clock.instance.nanoTime() - lastPause < MAX_LOCAL_PAUSE_IN_NANOS){logger.debug("Still not marking nodes down due to local pause");return;}double phi = hbWnd.phi(now);if (logger.isTraceEnabled())logger.trace("PHI for {} : {}", ep, phi);if (PHI_FACTOR * phi > getPhiConvictThreshold()){if (logger.isTraceEnabled())logger.trace("Node {} phi {} > {}; intervals: {} mean: {}", new Object[]{ep, PHI_FACTOR * phi, getPhiConvictThreshold(), hbWnd, hbWnd.mean()});for (IFailureDetectionEventListener listener : fdEvntListeners){listener.convict(ep, phi);}}else if (logger.isDebugEnabled() && (PHI_FACTOR * phi * DEBUG_PERCENTAGE / 100.0 > getPhiConvictThreshold())){logger.debug("PHI for {} : {}", ep, phi);}else if (logger.isTraceEnabled()){logger.trace("PHI for {} : {}", ep, phi);logger.trace("mean for {} : {}", ep, hbWnd.mean());}}//......
}
复制代码
  • FailureDetector实现了IFailureDetector, FailureDetectorMBean接口
  • 这里定义的PHI_FACTOR为1.0 / Math.log(10.0),而phiConvictThreshold默认为8;这里维护了arrivalSamples,即InetAddress及其ArrivalWindow的映射
  • 其isAlive方法取的epState.isAlive()的值;其interpret方法调用ArrivalWindow.phi计算now值的phi,然后乘以PHI_FACTOR,如果大于phiConvictThreshold则会回调IFailureDetectionEventListener的convict方法

EndpointState

cassandra-3.11.4/src/java/org/apache/cassandra/gms/EndpointState.java

public class EndpointState
{protected static final Logger logger = LoggerFactory.getLogger(EndpointState.class);public final static IVersionedSerializer<EndpointState> serializer = new EndpointStateSerializer();private volatile HeartBeatState hbState;private final AtomicReference<Map<ApplicationState, VersionedValue>> applicationState;/* fields below do not get serialized */private volatile long updateTimestamp;private volatile boolean isAlive;public boolean isAlive(){return isAlive;}void markAlive(){isAlive = true;}void markDead(){isAlive = false;}//......
}
复制代码
  • EndpointState的isAlive返回的是isAlive值,则markDead方法则会标记该值为false

ArrivalWindow

cassandra-3.11.4/src/java/org/apache/cassandra/gms/FailureDetector.java

class ArrivalWindow
{private static final Logger logger = LoggerFactory.getLogger(ArrivalWindow.class);private long tLast = 0L;private final ArrayBackedBoundedStats arrivalIntervals;private double lastReportedPhi = Double.MIN_VALUE;// in the event of a long partition, never record an interval longer than the rpc timeout,// since if a host is regularly experiencing connectivity problems lasting this long we'd// rather mark it down quickly instead of adapting// this value defaults to the same initial value the FD is seeded withprivate final long MAX_INTERVAL_IN_NANO = getMaxInterval();ArrivalWindow(int size){arrivalIntervals = new ArrayBackedBoundedStats(size);}private static long getMaxInterval(){String newvalue = System.getProperty("cassandra.fd_max_interval_ms");if (newvalue == null){return FailureDetector.INITIAL_VALUE_NANOS;}else{logger.info("Overriding FD MAX_INTERVAL to {}ms", newvalue);return TimeUnit.NANOSECONDS.convert(Integer.parseInt(newvalue), TimeUnit.MILLISECONDS);}}synchronized void add(long value, InetAddress ep){assert tLast >= 0;if (tLast > 0L){long interArrivalTime = (value - tLast);if (interArrivalTime <= MAX_INTERVAL_IN_NANO){arrivalIntervals.add(interArrivalTime);logger.trace("Reporting interval time of {} for {}", interArrivalTime, ep);}else{logger.trace("Ignoring interval time of {} for {}", interArrivalTime, ep);}}else{// We use a very large initial interval since the "right" average depends on the cluster size// and it's better to err high (false negatives, which will be corrected by waiting a bit longer)// than low (false positives, which cause "flapping").arrivalIntervals.add(FailureDetector.INITIAL_VALUE_NANOS);}tLast = value;}double mean(){return arrivalIntervals.mean();}// see CASSANDRA-2597 for an explanation of the math at work here.double phi(long tnow){assert arrivalIntervals.mean() > 0 && tLast > 0; // should not be called before any samples arrivelong t = tnow - tLast;lastReportedPhi = t / mean();return lastReportedPhi;}double getLastReportedPhi(){return lastReportedPhi;}public String toString(){return Arrays.toString(arrivalIntervals.getArrivalIntervals());}
}
复制代码
  • ArrivalWindow使用ArrayBackedBoundedStats来存储arrivalIntervals值
  • 其add方法是一个synchronized方法,它在tLast大于0且interArrivalTime小于等于MAX_INTERVAL_IN_NANO的时候才会执行arrivalIntervals.add(interArrivalTime),如果tLast小于等于0则执行arrivalIntervals.add(FailureDetector.INITIAL_VALUE_NANOS)
  • phi值采用了exponential distribution appropriate,即通过t / mean()来近似计算P(x <= t)

Although the original paper suggests that the distribution is approximated by the Gaussian distribution we found the Exponential Distribution to be a better approximation, because of the nature of the gossip channel and its impact on latency Regular message transmissions experiencing typical random jitter will follow a normal distribution, but since gossip messages from endpoint A to endpoint B are sent at random intervals, they likely make up a Poisson process, making the exponential distribution appropriate.

Gossiper

cassandra-3.11.4/src/java/org/apache/cassandra/gms/Gossiper.java

public class Gossiper implements IFailureDetectionEventListener, GossiperMBean
{//....../*** This method is part of IFailureDetectionEventListener interface. This is invoked* by the Failure Detector when it convicts an end point.** @param endpoint end point that is convicted.*/public void convict(InetAddress endpoint, double phi){EndpointState epState = endpointStateMap.get(endpoint);if (epState == null)return;if (!epState.isAlive())return;logger.debug("Convicting {} with status {} - alive {}", endpoint, getGossipStatus(epState), epState.isAlive());if (isShutdown(endpoint)){markAsShutdown(endpoint);}else{markDead(endpoint, epState);}}/*** This method is used to mark a node as shutdown; that is it gracefully exited on its own and told us about it* @param endpoint endpoint that has shut itself down*/protected void markAsShutdown(InetAddress endpoint){EndpointState epState = endpointStateMap.get(endpoint);if (epState == null)return;epState.addApplicationState(ApplicationState.STATUS, StorageService.instance.valueFactory.shutdown(true));epState.addApplicationState(ApplicationState.RPC_READY, StorageService.instance.valueFactory.rpcReady(false));epState.getHeartBeatState().forceHighestPossibleVersionUnsafe();markDead(endpoint, epState);FailureDetector.instance.forceConviction(endpoint);}@VisibleForTestingpublic void markDead(InetAddress addr, EndpointState localState){if (logger.isTraceEnabled())logger.trace("marking as down {}", addr);localState.markDead();liveEndpoints.remove(addr);unreachableEndpoints.put(addr, System.nanoTime());logger.info("InetAddress {} is now DOWN", addr);for (IEndpointStateChangeSubscriber subscriber : subscribers)subscriber.onDead(addr, localState);if (logger.isTraceEnabled())logger.trace("Notified {}", subscribers);}//......
}
复制代码
  • Gossiper实现了IFailureDetectionEventListener接口,其convict方法会获取endpointState,如果已经shutdown则执行markAsShutdown方法,否则执行markDead方法
  • markAsShutdown方法会调用markDead方法,然后再调用FailureDetector.instance.forceConviction(endpoint)方法
  • markDead方法则直接调用endpointState.markDead()方法,然后回调IEndpointStateChangeSubscriber的onDead方法

GossipTask

cassandra-3.11.4/src/java/org/apache/cassandra/gms/Gossiper.java

public class Gossiper implements IFailureDetectionEventListener, GossiperMBean
{//......public void start(int generationNumber){start(generationNumber, new EnumMap<ApplicationState, VersionedValue>(ApplicationState.class));}/*** Start the gossiper with the generation number, preloading the map of application states before starting*/public void start(int generationNbr, Map<ApplicationState, VersionedValue> preloadLocalStates){buildSeedsList();/* initialize the heartbeat state for this localEndpoint */maybeInitializeLocalState(generationNbr);EndpointState localState = endpointStateMap.get(FBUtilities.getBroadcastAddress());localState.addApplicationStates(preloadLocalStates);//notify snitches that Gossiper is about to startDatabaseDescriptor.getEndpointSnitch().gossiperStarting();if (logger.isTraceEnabled())logger.trace("gossip started with generation {}", localState.getHeartBeatState().getGeneration());scheduledGossipTask = executor.scheduleWithFixedDelay(new GossipTask(),Gossiper.intervalInMillis,Gossiper.intervalInMillis,TimeUnit.MILLISECONDS);}private class GossipTask implements Runnable{public void run(){try{//wait on messaging service to start listeningMessagingService.instance().waitUntilListening();taskLock.lock();/* Update the local heartbeat counter. */endpointStateMap.get(FBUtilities.getBroadcastAddress()).getHeartBeatState().updateHeartBeat();if (logger.isTraceEnabled())logger.trace("My heartbeat is now {}", endpointStateMap.get(FBUtilities.getBroadcastAddress()).getHeartBeatState().getHeartBeatVersion());final List<GossipDigest> gDigests = new ArrayList<GossipDigest>();Gossiper.instance.makeRandomGossipDigest(gDigests);if (gDigests.size() > 0){GossipDigestSyn digestSynMessage = new GossipDigestSyn(DatabaseDescriptor.getClusterName(),DatabaseDescriptor.getPartitionerName(),gDigests);MessageOut<GossipDigestSyn> message = new MessageOut<GossipDigestSyn>(MessagingService.Verb.GOSSIP_DIGEST_SYN,digestSynMessage,GossipDigestSyn.serializer);/* Gossip to some random live member */boolean gossipedToSeed = doGossipToLiveMember(message);/* Gossip to some unreachable member with some probability to check if he is back up */maybeGossipToUnreachableMember(message);/* Gossip to a seed if we did not do so above, or we have seen less nodesthan there are seeds.  This prevents partitions where each group of nodesis only gossiping to a subset of the seeds.The most straightforward check would be to check that all the seeds have beenverified either as live or unreachable.  To avoid that computation each round,we reason that:either all the live nodes are seeds, in which case non-seeds that come onlinewill introduce themselves to a member of the ring by definition,or there is at least one non-seed node in the list, in which case eventuallysomeone will gossip to it, and then do a gossip to a random seed from thegossipedToSeed check.See CASSANDRA-150 for more exposition. */if (!gossipedToSeed || liveEndpoints.size() < seeds.size())maybeGossipToSeed(message);doStatusCheck();}}catch (Exception e){JVMStabilityInspector.inspectThrowable(e);logger.error("Gossip error", e);}finally{taskLock.unlock();}}}private void doStatusCheck(){if (logger.isTraceEnabled())logger.trace("Performing status check ...");long now = System.currentTimeMillis();long nowNano = System.nanoTime();long pending = ((JMXEnabledThreadPoolExecutor) StageManager.getStage(Stage.GOSSIP)).metrics.pendingTasks.getValue();if (pending > 0 && lastProcessedMessageAt < now - 1000){// if some new messages just arrived, give the executor some time to work on themUninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);// still behind?  something's brokeif (lastProcessedMessageAt < now - 1000){logger.warn("Gossip stage has {} pending tasks; skipping status check (no nodes will be marked down)", pending);return;}}Set<InetAddress> eps = endpointStateMap.keySet();for (InetAddress endpoint : eps){if (endpoint.equals(FBUtilities.getBroadcastAddress()))continue;FailureDetector.instance.interpret(endpoint);EndpointState epState = endpointStateMap.get(endpoint);if (epState != null){// check if this is a fat client. fat clients are removed automatically from// gossip after FatClientTimeout.  Do not remove dead states here.if (isGossipOnlyMember(endpoint)&& !justRemovedEndpoints.containsKey(endpoint)&& TimeUnit.NANOSECONDS.toMillis(nowNano - epState.getUpdateTimestamp()) > fatClientTimeout){logger.info("FatClient {} has been silent for {}ms, removing from gossip", endpoint, fatClientTimeout);removeEndpoint(endpoint); // will put it in justRemovedEndpoints to respect quarantine delayevictFromMembership(endpoint); // can get rid of the state immediately}// check for dead state removallong expireTime = getExpireTimeForEndpoint(endpoint);if (!epState.isAlive() && (now > expireTime)&& (!StorageService.instance.getTokenMetadata().isMember(endpoint))){if (logger.isDebugEnabled()){logger.debug("time is expiring for endpoint : {} ({})", endpoint, expireTime);}evictFromMembership(endpoint);}}}if (!justRemovedEndpoints.isEmpty()){for (Entry<InetAddress, Long> entry : justRemovedEndpoints.entrySet()){if ((now - entry.getValue()) > QUARANTINE_DELAY){if (logger.isDebugEnabled())logger.debug("{} elapsed, {} gossip quarantine over", QUARANTINE_DELAY, entry.getKey());justRemovedEndpoints.remove(entry.getKey());}}}}//......
}
复制代码
  • Gossiper定义了start方法,该方法通过executor.scheduleWithFixedDelay创建了GossipTask的调度任务
  • GossipTask的run方法会执行doGossipToLiveMember、maybeGossipToUnreachableMember,最后执行doStatusCheck方法
  • doStatusCheck方法会遍历endpointStateMap中的InetAddress,对其执行FailureDetector.instance.interpret(endpoint)

小结

  • IFailureDetector接口定义了isAlive、interpret、report、forceConviction、registerFailureDetectionEventListener、unregisterFailureDetectionEventListener方法
  • FailureDetector实现了IFailureDetector, FailureDetectorMBean接口;其isAlive方法取的epState.isAlive()的值,EndpointState的isAlive返回的是isAlive值,则markDead方法则会标记该值为false;其interpret方法调用ArrivalWindow.phi计算now值的phi,然后乘以PHI_FACTOR,如果大于phiConvictThreshold则会回调IFailureDetectionEventListener的convict方法
  • ArrivalWindow使用ArrayBackedBoundedStats来存储arrivalIntervals值;其add方法是一个synchronized方法,它在tLast大于0且interArrivalTime小于等于MAX_INTERVAL_IN_NANO的时候才会执行arrivalIntervals.add(interArrivalTime),如果tLast小于等于0则执行arrivalIntervals.add(FailureDetector.INITIAL_VALUE_NANOS);phi值采用了exponential distribution appropriate,即通过t / mean()来近似计算P(x <= t)
  • Gossiper实现了IFailureDetectionEventListener接口,其convict方法会获取endpointState,如果已经shutdown则执行markAsShutdown方法,否则执行markDead方法;markAsShutdown方法会调用markDead方法,然后再调用FailureDetector.instance.forceConviction(endpoint)方法;markDead方法则直接调用endpointState.markDead()方法,然后回调IEndpointStateChangeSubscriber的onDead方法
  • Gossiper定义了start方法,该方法通过executor.scheduleWithFixedDelay创建了GossipTask的调度任务;GossipTask的run方法会执行doGossipToLiveMember、maybeGossipToUnreachableMember,最后执行doStatusCheck方法;doStatusCheck方法会遍历endpointStateMap中的InetAddress,对其执行FailureDetector.instance.interpret(endpoint)

doc

  • The Phi Accrual Failure Detector by Hayashibara et al
  • Cassandra - A Decentralized Structured Storage System
  • inconsistent implementation of 'cumulative distribution function' for Exponential Distribution
  • Cassandra中失效检测原理详解
  • cassandra中对节点失败与否的探测方法, the Phi accrual Failure Dector,附论文

转载于:https://juejin.im/post/5cc908e3e51d453ae219578e

聊聊Cassandra的FailureDetector相关推荐

  1. Cassandra数据修复失败问题

    Cassandra数据修复失败问题 转自:Cassandra数据修复失败问题 背景 为了保证cassandra不同节点数据的一致性,需要定期进行repair操作.但是,当数据量达到一定规模时,repa ...

  2. 聊聊 Spring Boot 2.x 那些事儿

    本文目录: 即将的 Spring 2.0 - Spring 2.0 是什么 - 开发环境和 IDE - 使用 Spring Initializr 快速入门 Starter 组件 - Web:REST ...

  3. Cassandra Gossip协议的二三事儿

    Gossip协议是Cassandra维护各节点状态的一个重要组件,下面我们以Gossip协议三次握手为线索逐步分析Gossip协议源码. Gossip协议通过判断节点的generation和versi ...

  4. 从618大促聊聊华为云GaussDB NoSQL的蓬勃张力

    摘要:一年一度的 618大促,各大平台在不同的时期推出各种购物促销活动.对于这种瞬时性较强.业务处理量突然爆发的情况来说,业务系统的CPU/内存资源.网络传输成了关键瓶颈,云服务的弹性伸缩成为的最为关 ...

  5. HBase、Cassandra、LevelDB、RocksDB底层数据结构是什么?

    大家好,我是球哥.没啥球用的球,目前在965互联网公司做架构.今天分享开源数据框架中最最常用的底层数据结构. 一.概述 当前已被广泛运用在一些开源的数据产品中,如:HBase.Cassandra.Le ...

  6. 聊聊 Spring Boot 2.0 的 WebFlux

    https://zhuanlan.zhihu.com/p/30813274 首发于极乐科技 写文章登录 聊聊 Spring Boot 2.0 的 WebFlux 泥瓦匠BYSocket 4 个月前 聊 ...

  7. cassandra nodetool 指令

    CASSANDRA NODETOOL 指令翻译 用法:nodetool [(-u <用户名> | --username <用户名>)] [(-pw <密码> | – ...

  8. 聊聊后端程序员的知识体系-第一篇

    聊聊后端程序员的知识体系-第一篇 原文链接:https://www.fpthinker.com/backend_knowledge_architecture/knowledge.htmll 亲爱的读者 ...

  9. Cassandra 1.2 发布,NoSQL 数据库

    NoSQL 数据库 Cassandra 发布 1.2 正式版,该版本包含 CQL3,这是在 2012年4月发布的 1.1 版本中引入的.CQL 是一个 Cassandra 的建模和查询语言,类似关系数 ...

最新文章

  1. Dijkstra(迪杰斯特拉)算法简介
  2. Linux:检查当前运行级别的五种方法
  3. Visual Studio 2012/2010/2008 远程调试
  4. 带你刷burpsuite官方网络安全学院靶场(练兵场)之客户端漏洞——跨站请求伪造(CSRF)专题
  5. C++空指针访问成员函数
  6. 【Get 以太坊技能】CentOS 7 安装 go
  7. Git标签tag及tag远程同步
  8. CLR线程概览(一)
  9. 物联网企业该如何与华为云合作,这份FAQ值得一看
  10. swift 自定义TabBarItem
  11. python 柱形图_如何利用python 中的pyecharts包绘制柱形图
  12. anaconda r 语言_Centos7系统下R、 Rstudio及sparklyr的安装与配置
  13. DBS:CUPhone
  14. 删除excel 2007数据透视表
  15. 不容错过的30页超赞项目管理PPT
  16. np.stack()函数详解
  17. 敏捷项目管理 第2版[JimHighsmith](一)
  18. 计算机管理员解除阻止程序方法,电脑安装软件时弹出系统管理员设置了系统策略,禁止进行此安装解决方法...
  19. wxpython之入门
  20. 浅谈数学、数学建模与人工智能(机器学习,深度学习)之间的关系?

热门文章

  1. oracle中master实例,oracle基础(基本介绍)
  2. tf.keras.losses.CategoricalCrossentropy 多分类 交叉熵 损失函数示例
  3. 二值网络--Optimize Deep Convolutional Neural Network with Ternarized Weights and High Accuracy
  4. 关于Java为什么配置好环境变量但是不能在命令行cmd运行javac的问题
  5. centos7 解决chrome提示您的连接不是私密连接的方法
  6. java微积分计算步骤_一次刨根问底的收获——从一道微积分题说开去
  7. 服务器系统需要定期清理吗,windows 2008服务器系统清理
  8. matlab球坐标曲线,matlab绘制曲线subplotsphere球面坐标绘制饼图
  9. java 遍历list 性能_java list三种遍历方法性能比較
  10. 面试官:说说什么是Java内存模型?