Envoy 源码分析--程序启动过程
Envoy 源码分析--程序启动过程
申明:本文的 Envoy 源码分析基于 Envoy1.10.0。
前面几章分析了 event事件 和 底层网络, 但对创建服务的过程并没有串起来,只是分析了底层的网络公共库。这次我们分析下整个服务的创建过程。
初始化
main 入口
服务启动的总入口 main 函数,会先创建 MainCommon。
int main(int argc, char** argv) {
  ... ...
  std::unique_ptr<Envoy::MainCommon> main_common;
  try {
    main_common = std::make_unique<Envoy::MainCommon>(argc, argv);
  ... ...
}
MainCommon 初始化
MainCommon 在构造函数时,会先解析程序的参数,然后再调用 MainCommonBase。
MainCommon::MainCommon(int argc, const char* const* argv)
    : options_(argc, argv, &MainCommon::hotRestartVersion, spdlog::level::info),
      base_(options_, real_time_system_, default_test_hooks_,prod_component_factory_,
      std::make_unique<Runtime::RandomGeneratorImpl>(),platform_impl_.threadFactory(),
      platform_impl_.fileSystem()) {}
OptionsImpl 使用开源的 tclap 解析库。OptionsImpl 支持很多参数配置,具体的参数配置参考 operation/cli。
MainCommonBase 会初始化全局的参数,接着调用服务进行初始化。
MainCommonBase::MainCommonBase(... ...)
    : options_(options), component_factory_(component_factory), thread_factory_(thread_factory),file_system_(file_system) {
  //全局的或第三方库预先初始化
  Thread::ThreadFactorySingleton::set(&thread_factory_);
  ares_library_init(ARES_LIB_INIT_ALL);
  Event::Libevent::Global::initialize();
  RELEASE_ASSERT(Envoy::Server::validateProtoDescriptors(), "");
  Http::Http2::initializeNghttp2Logging();
  ... ... 
  //初始化服务
  server_ = std::make_unique<Server::InstanceImpl>(
    options_, time_system, local_address, test_hooks, *restarter_, *stats_store_,
    access_log_lock, component_factory, std::move(random_generator), *tls_, thread_factory_,
    file_system_);
服务 InstanceImpl 初始化
InstanceImpl 主要是初始化 admin 管理服务,各个静态 XDS 的加载以及初始化服务。
InstanceImpl 在核心函数中先加载配置文件,通过参数 -c 配置。
void InstanceImpl::initialize(const Options& options,
                              Network::Address::InstanceConstSharedPtr local_address,
                              ComponentFactory& component_factory, TestHooks& hooks) {
  ... ...
  //加载Bootstrap
  InstanceUtil::loadBootstrapConfig(bootstrap_, options, *api_);
  bootstrap_config_update_time_ = time_source_.systemTime();
  ... ...
}
配置文件是一个 json 格式,包括以下几项:
- node:节点标识,会在管理服务器中呈现,用于标识目的(例如,头域中生成相应的字段)。
- static_resources:指定静态资源配置。
- dynamic_resources:动态发现服务源配置。
- cluster_manager:该服务所有的上游群集的群集管理配置。
- hds_config:服务健康检查配置。
- flags_path:用于启动文件标志的路径。
- stats_sinks:统计汇总设置
- stats_config:配置内部处理统计信息。
- stats_flush_interval:刷新到统计信息服务的周期时间。出于性能方面的考虑,Envoy锁定计数器,并且只是周期性地刷新计数器和计量器。如果未指定,则默认值为5000毫秒(5秒)。
- watchdog:看门狗配置。
- tracing:配置外置的追踪服务程序。如果没有指定,则不会执行追踪。
- runtime:配置运行时配置分发服务程序。
- admin: 配置本地管理的HTTP服务。
- overload_manager:过载管理配置(资源限制)。
各个项的具体作用和配置参考Bootstrap
加载完配置项,接着会启动 admin 本地的HTTP服务。
初始化 admin 服务
admin 先创建一个 AdminImpl,在构造函数里初始化 URI。
AdminImpl::AdminImpl(const std::string& profile_path, Server::Instance& server)
    : server_(server), profile_path_(profile_path),
      ... ...
      handlers_{
          {"/", "Admin home page", MAKE_ADMIN_HANDLER(handlerAdminHome), false, false},
          {"/certs", "print certs on machine", MAKE_ADMIN_HANDLER(handlerCerts), false, false},
          // 导出 cluster  统计信息
          {"/clusters", "upstream cluster status", MAKE_ADMIN_HANDLER(handlerClusters), false,
           false},
          // 导出配置文件
          {"/config_dump", "dump current Envoy configs (experimental)",
           MAKE_ADMIN_HANDLER(handlerConfigDump), false, false},
          // 导出连接统计信息
          {"/contention", "dump current Envoy mutex contention stats (if enabled)",
           MAKE_ADMIN_HANDLER(handlerContention), false, false},
          ... ...
      admin_filter_chain_(std::make_shared<AdminFilterChain>()) {}
接着启动一个服务。
admin_->startHttpListener(initial_config.admin().accessLogPath(), options.adminAddressPath(),
                              initial_config.admin().address(),
                              stats_store_.createScope("listener.admin."));
在启动服务函数内部会创建 TcpListenSocket 和 AdminListener。
void AdminImpl::startHttpListener(const std::string& access_log_path,
                                  const std::string& address_out_path,
                                  Network::Address::InstanceConstSharedPtr address,
                                  Stats::ScopePtr&& listener_scope) {
  ... ...
  socket_ = std::make_unique<Network::TcpListenSocket>(address, nullptr, true);
  listener_ = std::make_unique<AdminListener>(*this, std::move(listener_scope));
  ... ...
}
初始化 TcpListenSocket 时会在内部创建一个 socket 后再进行 bind。
using TcpListenSocket = NetworkListenSocket<NetworkSocketTrait<Address::SocketType::Stream>>;
template <typename T> class NetworkListenSocket : public ListenSocketImpl {
public:
  NetworkListenSocket(const Address::InstanceConstSharedPtr& address,
                      const Network::Socket::OptionsSharedPtr& options, bool bind_to_port)
      // socket 创建
      : ListenSocketImpl(address->socket(T::type), address) {
    RELEASE_ASSERT(io_handle_->fd() != -1, "");
    setPrebindSocketOptions();
    setupSocket(options, bind_to_port);
  }
void ListenSocketImpl::setupSocket(const Network::Socket::OptionsSharedPtr& options, bool bind_to_port) {
  setListenSocketOptions(options);
  //准备进行绑定
  if (bind_to_port) {
    doBind();
  }
}
void ListenSocketImpl::doBind() {
  //绑定
  const Api::SysCallIntResult result = local_address_->bind(io_handle_->fd());
  ... ...
}
AdminListener 构造函数内只是参数的初始化。
AdminListener(AdminImpl& parent, Stats::ScopePtr&& listener_scope)
  : parent_(parent), name_("admin"), scope_(std::move(listener_scope)),
    stats_(Http::ConnectionManagerImpl::generateListenerStats("http.admin.", *scope_)) {}
做完 socket ,bind 后面就是进行 listen 处理。将 AdminListener 通过 handler 加入监听队列。handler 是在 InstanceImpl 的构造函数内初始化的。
InstanceImpl::InstanceImpl(... ...)
  : handler_(new ConnectionHandlerImpl(ENVOY_LOGGER(), *dispatcher_)),
  ... ...{
  }
void InstanceImpl::initialize(... ...)
{
  ... ...
  //将 AdminListener 加入 ConnectionHandler
  if (initial_config.admin().address()) {
    admin_->addListenerToHandler(handler_.get());
  }
  ... ...
}
void AdminImpl::addListenerToHandler(Network::ConnectionHandler* handler) {
  // 这里的 listener_ 就是上面生成的 AdminListener
  if (listener_) {
    handler->addListener(*listener_);
  }
}
在 addListener 内会新建一个 ActiveListener 内部类,先置为 disable 状态。
void ConnectionHandlerImpl::addListener(Network::ListenerConfig& config) {
  ActiveListenerPtr l(new ActiveListener(*this, config));
  if (disable_listeners_) {
    l->listener_->disable();
  }
  listeners_.emplace_back(config.socket().localAddress(), std::move(l));
}
在 ActiveListener 构造函数内创建 listen,里面dispatcher 会创建回调。等有新连接到来时,会回调 onAccept.
ConnectionHandlerImpl::ActiveListener::ActiveListener(ConnectionHandlerImpl& parent,Network::ListenerConfig& config)
    : ActiveListener(
          parent,
          // 创建listen
          parent.dispatcher_.createListener(config.socket(), *this, config.bindToPort(),
                                            config.handOffRestoredDestinationConnections()),
          config) {}
Network::ListenerPtr
DispatcherImpl::createListener(Network::Socket& socket, Network::ListenerCallbacks& cb, bool bind_to_port, bool hand_off_restored_destination_connections) {
  ASSERT(isThreadSafe());
  return Network::ListenerPtr{new Network::ListenerImpl(*this, socket, cb, bind_to_port,
                                                        hand_off_restored_destination_connections)};
}
void ListenerImpl::setupServerSocket(Event::DispatcherImpl& dispatcher, Socket& socket) {
  listener_.reset(
      //创建 evconnlistener_new ,有连接回调listenCallback
      evconnlistener_new(&dispatcher.base(), listenCallback, this, 0, -1, socket.ioHandle().fd()));
  ... ...
}
void ListenerImpl::listenCallback(evconnlistener*, evutil_socket_t fd, sockaddr* remote_addr,int remote_addr_len, void* arg) {
  ... ...
  //回调ActiveListener的onAccept
  listener->cb_.onAccept(
    std::make_unique<AcceptedSocketImpl>(std::move(io_handle), local_address, remote_address),
    listener->hand_off_restored_destination_connections_);
}
onAccept 对 Listern 过滤后,创建新连接。
void ConnectionHandlerImpl::ActiveListener::onAccept()
{
  ... ...
  active_socket->continueFilterChain(true);
  ... ...
}
void ConnectionHandlerImpl::ActiveSocket::continueFilterChain(bool success) {
  ... ...
   listener_.newConnection(std::move(socket_));
  ... ...
}
void ConnectionHandlerImpl::ActiveListener::onNewConnection() {
  ... ...
  if (new_connection->state() != Network::Connection::State::Closed) {
    ActiveConnectionPtr active_connection(
        new ActiveConnection(*this, std::move(new_connection), parent_.dispatcher_.timeSource()));
    active_connection->moveIntoList(std::move(active_connection), connections_);
    parent_.num_connections_++;
  }
  ... ...
}
这样,新连接就建立起来。
配置文件 XDS 初始化
初始化 Bootstrap 的 XDS 时,先初始化 static sercret,先初始化 cluster,接着初始化 listeners。
void MainImpl::initialize(... ...) {
  //初始化secrets
  const auto& secrets = bootstrap.static_resources().secrets();
  for (ssize_t i = 0; i < secrets.size(); i++) {
    ENVOY_LOG(debug, "static secret #{}: {}", i, secrets[i].name());
    server.secretManager().addStaticSecret(secrets[i]);
  }
  //初始化 cluster
  bootstrap.static_resources().clusters().size());
  cluster_manager_ = cluster_manager_factory.clusterManagerFromProto(bootstrap);
  // 初始化listeners
  const auto& listeners = bootstrap.static_resources().listeners();
  for (ssize_t i = 0; i < listeners.size(); i++) {
    ENVOY_LOG(debug, "listener #{}:", i);
    server.listenerManager().addOrUpdateListener(listeners[i], "", false);
  }
初始化 cluster 会分两阶段初始化。先初始化非 EDS 部分,再初始化 EDS 部分。分两个阶段的初始化是因为在 v2 配置中每个 EDS 集群单独设置订阅。此订阅是 API 源时 群集将依赖于非 EDS 群集,因此必须首先初始化非 EDS 群集。cluster 的类型有 5个类型:
enum DiscoveryType {
    // Refer to the :ref:`static discovery
    STATIC = 0;
    // Refer to the :ref:`strict DNS discovery
    STRICT_DNS = 1;
    // Refer to the :ref:`logical DNS discovery
    LOGICAL_DNS = 2;
    // Refer to the :ref:`service discovery
    EDS = 3;
    // Refer to the :ref:`original destination discovery
    ORIGINAL_DST = 4;
  }
cluster 初始化顺序:
非 EDS 部分 -> ADS -> EDS -> CDS
ClusterManagerImpl::ClusterManagerImpl(... ...) {
  ... ...
  for (const auto& cluster : bootstrap.static_resources().clusters()) {
    // 第一次初始化非 EDS 部分
    if (cluster.type() != envoy::api::v2::Cluster::EDS) {
      loadCluster(cluster, "", false, active_clusters_);
    }
  }
  // 初始化 ADS
  if (bootstrap.dynamic_resources().has_ads_config()) {
    ads_mux_ = std::make_unique<Config::GrpcMuxImpl>(
        local_info,
        Config::Utility::factoryForGrpcApiConfigSource(
            *async_client_manager_, bootstrap.dynamic_resources().ads_config(), stats)
            ->create(),
        main_thread_dispatcher,
        *Protobuf::DescriptorPool::generated_pool()->FindMethodByName(
            "envoy.service.discovery.v2.AggregatedDiscoveryService.StreamAggregatedResources"),
        random_, stats_,
        Envoy::Config::Utility::parseRateLimitSettings(bootstrap.dynamic_resources().ads_config()));
  } else {
    ads_mux_ = std::make_unique<Config::NullGrpcMuxImpl>();
  }
  for (const auto& cluster : bootstrap.static_resources().clusters()) {
    // 初始化 EDS
    if (cluster.type() == envoy::api::v2::Cluster::EDS) {
      loadCluster(cluster, "", false, active_clusters_);
    }
  }
  ... ...
  //初始化 CDS
  if (bootstrap.dynamic_resources().has_cds_config()) {
    cds_api_ = factory_.createCds(bootstrap.dynamic_resources().cds_config(), *this);
    init_helper_.setCds(cds_api_.get());
  } else {
    init_helper_.setCds(nullptr);
  }
}
上面都初始化完成后,再初始化 lds,最后再初始化 hds。
// 初始化lds
if (bootstrap_.dynamic_resources().has_lds_config()) {
  listener_manager_->createLdsApi(bootstrap_.dynamic_resources().lds_config());
}
//初始化hds
if (bootstrap_.has_hds_config()) {
  const auto& hds_config = bootstrap_.hds_config();
  async_client_manager_ = std::make_unique<Grpc::AsyncClientManagerImpl>(
      *config_.clusterManager(), thread_local_, time_source_, *api_);
  ... ...
}
初始化 ListenerManager
ListenerManager 的初始化只是事先创建 worker。
ListenerManagerImpl::ListenerManagerImpl(... ...) {
  ... ...
  // 创建worker子线程
  for (uint32_t i = 0; i < server.options().concurrency(); i++) {
    workers_.emplace_back(worker_factory.createWorker(server.overloadManager()));
  }
}
WorkerPtr ProdWorkerFactory::createWorker(OverloadManager& overload_manager) {
  // 新建子线程,每个线种一个dispatchr
  Event::DispatcherPtr dispatcher(api_.allocateDispatcher());
  return WorkerPtr{new WorkerImpl(
      tls_, hooks_, std::move(dispatcher),
      Network::ConnectionHandlerPtr{new ConnectionHandlerImpl(ENVOY_LOGGER(), *dispatcher)},
      overload_manager, api_)};
}
WorkerImpl::WorkerImpl(... ...)
    : tls_(tls), hooks_(hooks), dispatcher_(std::move(dispatcher)), handler_(std::move(handler)), api_(api) {
  tls_.registerThread(*dispatcher_, false);
  overload_manager.registerForAction(
      OverloadActionNames::get().StopAcceptingConnections, *dispatcher_,
      [this](OverloadActionState state) { stopAcceptingConnectionsCb(state); });
}
启动
main 启动入口
main 函数调用 main_common
int main(int argc, char** argv) {
  ... ...
  return main_common->run() ? EXIT_SUCCESS : EXIT_FAILURE;
}
main_common 进一步调用 InstanceImpl
bool MainCommonBase::run() {
  switch (options_.mode()) {
  case Server::Mode::Serve:
    server_->run();
    return true;
  ... ...
}
InstanceImpl 启用 loop 循环。
void InstanceImpl::run() {
  auto run_helper = RunHelper(*this, options_, *dispatcher_, clusterManager(), access_log_manager_,
                              init_manager_, overloadManager(), [this] { startWorkers(); });
  auto watchdog = guard_dog_->createWatchDog(api_->threadFactory().currentThreadId());
  watchdog->startWatchdog(*dispatcher_);
  dispatcher_->post([this] { notifyCallbacksForStage(Stage::Startup); });
  dispatcher_->run(Event::Dispatcher::RunType::Block);
  ENVOY_LOG(info, "main dispatch loop exited");
  guard_dog_->stopWatching(watchdog);
  watchdog.reset();
  terminate();
}
服务启动流程
本地 HTTP 管理服务的启动流程上面已经分析过,现在讨论本地服务的启动流程(XDS 下发的暂不讨论)。
在 cluster 初始化的时候,加入 listener。
void MainImpl::initialize(... ...) {
  ... ...
  // 初始化listeners
  const auto& listeners = bootstrap.static_resources().listeners();
  for (ssize_t i = 0; i < listeners.size(); i++) {
    ENVOY_LOG(debug, "listener #{}:", i);
    server.listenerManager().addOrUpdateListener(listeners[i], "", false);
  }
}
addOrUpdateListener 创建 ListenerImpl,ListenerImpl 做 bind 操作。
  // 创建ListenerImpl
  ListenerImplPtr new_listener(
      new ListenerImpl(config, version_info, *this, name, modifiable, workers_started_, hash));
  ListenerImpl& new_listener_ref = *new_listener;
  ... ...
  //bind 地址将 socket 关联ListenerImpl
  new_listener->setSocket(draining_listener_socket
                                ? draining_listener_socket
                                : factory_.createListenSocket(new_listener->address(),
                                                              new_listener->socketType(),
                                                              new_listener->listenSocketOptions(),
                                                              new_listener->bindToPort()));
Network::SocketSharedPtr ProdListenerComponentFactory::createListenSocket(... ...) {
  ... ...
  // 调用 UdsListenSocket 做 bind() 操作。
  if (io_handle->isOpen()) {
      return std::make_shared<Network::UdsListenSocket>(std::move(io_handle), address);
  }
  return std::make_shared<Network::UdsListenSocket>(address);
}
// 最终调用系统bind()操作
void ListenSocketImpl::doBind() {
  const Api::SysCallIntResult result = local_address_->bind(io_handle_->fd());
  ... ...
}
在 InstanceImpl 启动时,调用 RunHelper。RunHelper 则启动 startWorkers。startWorker 将初始化得到的 listeners 加入到 work 中。
void ListenerManagerImpl::startWorkers(GuardDog& guard_dog) {
  workers_started_ = true;
  for (const auto& worker : workers_) {
    ASSERT(warming_listeners_.empty());
    for (const auto& listener : active_listeners_) {
      addListenerToWorker(*worker, *listener);
    }
    worker->start(guard_dog);
  }
}
work 将 linsteners 关联到 connectioHandler。
void ListenerManagerImpl::addListenerToWorker(Worker& worker, ListenerImpl& listener) {
  worker.addListener(listener, [this, &listener](bool success) -> void {
  ... ...
}
void WorkerImpl::addListener(Network::ListenerConfig& listener, AddListenerCompletion completion) {
  dispatcher_->post([this, &listener, completion]() -> void {
    try {
      // 关联到connectioHandler。
      handler_->addListener(listener);
      hooks_.onWorkerListenerAdded();
      completion(true);
    } catch (const Network::CreateListenerException& e) {
      completion(false);
    }
  });
}
connectioHandler 在 work 初始化时创建。
ListenerManagerImpl::ListenerManagerImpl(Instance& server,
                                         ListenerComponentFactory& listener_factory,
                                         WorkerFactory& worker_factory)
    : server_(server), factory_(listener_factory), stats_(generateStats(server.stats())),
      config_tracker_entry_(server.admin().getConfigTracker().add(
          "listeners", [this] { return dumpListenerConfigs(); })) {
  for (uint32_t i = 0; i < server.options().concurrency(); i++) {
    // 初始化worker
    workers_.emplace_back(worker_factory.createWorker(server.overloadManager()));
  }
}
WorkerPtr ProdWorkerFactory::createWorker(OverloadManager& overload_manager) {
  Event::DispatcherPtr dispatcher(api_.allocateDispatcher());
  return WorkerPtr{new WorkerImpl(
      tls_, hooks_, std::move(dispatcher),
      //创建connectioHandler
      Network::ConnectionHandlerPtr{new ConnectionHandlerImpl(ENVOY_LOGGER(), *dispatcher)},
      overload_manager, api_)};
}
将 linsteners 关联到 connectioHandler 后,后面的 listen(),accept() 和创建连接过程和 admin 的 HTTP 启动流程是一样的。
LDS 服务启动流程
整个服务的启动流程基本就完成了,后面有新加服务的启动流程和上面的服务启动流程一样,调用 addOrUpdateListener。在 addOrUpdateListener 内判断服务是否已启动,如果已启动调用 ManagerImpl 等待初始化。
void ListenerImpl::initialize() {
  last_updated_ = timeSource().systemTime();
  if (workers_started_) {
    //ManagerImpl
    dynamic_init_manager_.initialize(*init_watcher_);
  }
}
void ManagerImpl::initialize(const Watcher& watcher) {
  ... ...
    for (const auto& target_handle : target_handles_) {
      // 等待 target_handle 初始化完成。
      if (!target_handle->initialize(watcher_)) {
        onTargetReady();
      }
    }
}
初始化完成后,调用函数指针。函数指针在初始化WatcherImpl传入。
void ManagerImpl::onTargetReady() {
  if (--count_ == 0) {
    // 初始化完成
    ready();
  }
}
void ManagerImpl::ready() {
  state_ = State::Initialized;
  watcher_handle_->ready();
}
bool WatcherHandleImpl::ready() const {
    //调用函数指针
    (*locked_fn)();
}
ListenerImpl::ListenerImpl(... ...)
  : ... ...
   // 初始化watch
   init_watcher_(std::make_unique<Init::WatcherImpl>(
          "ListenerImpl", [this] { parent_.onListenerWarmed(*this); })){}
在 onListenerWarmed 内将 listener 加入 work。后面流程和 服务启动流程一样,不再分析。
void ListenerManagerImpl::onListenerWarmed(ListenerImpl& listener) {
  for (const auto& worker : workers_) {
    addListenerToWorker(*worker, listener);
  }
最后
整个服务的初始化和启动流程就完成了。服务的启动有3个类型 : 本地 HTTP 服务管理服务、本地配置文件的服务和xDS下发的服务。本章节只分析了服务的启动流程,连接成功的后继处理,以后分析。
Envoy 源码分析--程序启动过程的更多相关文章
- mybatis源码分析:启动过程
		mybatis在开发中作为一个ORM框架使用的比较多,所谓ORM指的是Object Relation Mapping,直译过来就是对象关系映射,这个映射指的是java中的对象和数据库中的记录的映射,也 ... 
- Nimbus<三>Storm源码分析--Nimbus启动过程
		Nimbus server, 首先从启动命令开始, 同样是使用storm命令"storm nimbus”来启动看下源码, 此处和上面client不同, jvmtype="-serv ... 
- workerman源码分析之启动过程
		PHP一直以来以草根示人,它简单,易学,被大量应用于web开发,非常可惜的是大部分开发都在简单的增删改查,或者加上pdo,redis等客户端甚至分布式,以及规避语言本身的缺陷.然而这实在太委屈PHP了 ... 
- elasticSearch6源码分析(1)启动过程
		1.找到bin目录,下面有elasticSearch的sh文件,查看执行过程 exec \ "$JAVA" \ $ES_JAVA_OPTS \ -Des.path.home=&qu ... 
- 1 cocos2dx源码分析-程序启动与主循环
		1 启动 在iOS系统中,由main函数启动默认调用了AppController main.m NSAutoreleasePool * pool = [[NSAutoreleasePool ... 
- Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)
		http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ... 
- Envoy 源码分析--LDS
		Envoy 源码分析--LDS LDS 是 Envoy 用来自动获取 listener 的 API. Envoy 通过 API 可以增加.修改或删除 listener. 先来总结下 listener ... 
- u-boot 源码分析(1) 启动过程分析
		u-boot 源码分析(1) 启动过程分析 文章目录 u-boot 源码分析(1) 启动过程分析 前言 配置 源码结构 api arch board common cmd drivers fs Kbu ... 
- SOFA 源码分析 —— 服务引用过程
		前言 在前面的 SOFA 源码分析 -- 服务发布过程 文章中,我们分析了 SOFA 的服务发布过程,一个完整的 RPC 除了发布服务,当然还需要引用服务. So,今天就一起来看看 SOFA 是如何引 ... 
随机推荐
- 从0开始学习 GITHUB 系列之「加入 GITHUB」【转】
			本文转载自:http://stormzhang.com/github/2016/05/26/learn-github-from-zero2/ 版权声明:本文为 stormzhang 原创文章,可以随意 ... 
- linux下如何关闭某个tmux窗口
			答:分成两个步骤,如下: 1.列出当前的tmux窗口 jello@jello:~$ tmux ls 1: 1 windows (created Tue Jan 17 09:28:05 2019) [2 ... 
- caffe2 环境的搭建以及detectron的配置
			caffe2 环境的搭建以及detectron的配置 建议大家看一下这篇博客https://tech.amikelive.com/node-706/comprehensive-guide-instal ... 
- 2012NOIP模拟试题
			做的时候觉得这套题好简单,结果一看发现是2012年的模拟题,估计就是普及+的难度吧,AK无压力 总结 第一题状压我智障的调了好几分钟,因为我的最终状态写的1<<n,智障了 第三题的dfs调 ... 
- eclipse中下载maven插件解决办法
			https://blog.csdn.net/qq_30546099/article/details/71195446 解决Eclipse Maven插件的最佳方案 https://www.cnblog ... 
- pycharm同时使用python2.7版本和python3.6版本
			最近在看爬虫的专题,很多爬虫的教程是python2的,电脑上装的是3.6版本,而且python不向下兼容,这就很麻烦,最简单的print要加括号啊,等等.于是分享一个在windows环境下pychar ... 
- android平台蓝牙编程
			Android平台支持蓝牙网络协议栈,实现蓝牙设备之间数据的无线传输. 本文档描述了怎样利用android平台提供的蓝牙API去实现蓝牙设备之间的通信,蓝牙设备之间的通信主要包括了四个步骤:设置蓝牙设 ... 
- 网络编程 单机最大tcp连接数
			在tcp应用中,server事先在某个固定端口监听,client主动发起连接,经过三路握手后建立tcp连接.那么对单机,其最大并发tcp连接数是多少? 如何标识一个TCP连接 在确定最大连接数之前,先 ... 
- appium+pytest+allure+jenkins 如何实现多台手机连接
			使用appium可以实现app自动化测试,我们之前是连接一台手机去运行,如何同时连接多台手机呢?很多人可能想到的是多线程(threading).今天分享一种比多线程更简单的方法,虽然不是多台手机同时运 ... 
- sed:轻量级流编辑器
			一. sed命令 sed是一种几乎包括在所有UNIX平台(包括Linux)的轻量级流编辑器.sed主要是用来将数据进行选取.替换.删除.新增的命令 注意:vi命令只能修改文件,但不能修改命令的结果,如 ... 
