ApolloClient.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. class ApolloClient
  3. {
  4. protected $configServer; //apollo服务端地址
  5. protected $appId; //apollo配置项目的appid
  6. protected $cluster = 'default';
  7. protected $clientIp = '127.0.0.1'; //绑定IP做灰度发布用
  8. protected $notifications = array();
  9. protected $pullTimeout = 10; //获取某个namespace配置的请求超时时间
  10. protected $intervalTimeout = 10; //每次请求获取apollo配置变更时的超时时间
  11. public $save_dir; //配置保存目录
  12. public $publicConfigDir; //公用部分配置路径
  13. private $checkNotificationsId=array(); //用于读取本地配置版本号,和线上对比(直接使用$notifications,发现读取接口超慢,所以另存一个变量)
  14. public $replaceValueArr=array();
  15. /**
  16. * ApolloClient constructor.
  17. * @param string $configServer apollo服务端地址
  18. * @param string $appId apollo配置项目的appid
  19. * @param array $namespaces apollo配置项目的namespace
  20. * @param string $cluster 集群名,和项目的testing,dev等环境变量相同
  21. * @param string $configFile 生成的配置文件地址
  22. * @param string $config 本地公共的配置文件地址(不需要放到配置中心的本地配置项,一般为不需要变更的配置)
  23. */
  24. public function __construct($configServer, $appId, array $namespaces,$cluster,$configFile,$config='')
  25. {
  26. $this->configServer = $configServer;
  27. $this->appId = $appId;
  28. $this->cluster=$cluster;
  29. $configFiles=array();
  30. foreach ($namespaces as $namespace) {
  31. $this->notifications[$namespace] = array('namespaceName' => $namespace, 'notificationId' => -1);
  32. $configFiles[$namespace]=$this->getConfigFileNameSpace($cluster,$namespace);
  33. }
  34. $this->save_dir = $configFile;
  35. $this->publicConfigDir=$config;
  36. //读取配置的版本号
  37. if($configFiles){
  38. foreach ($configFiles as $namespace=>$file){
  39. $filePath= $this->publicConfigDir.$this->getConfigFileNameSpace($cluster,$namespace,1);
  40. if(file_exists($filePath)){
  41. $configValues=require $filePath;
  42. if(isset($configValues['params']['notification'][$namespace])){
  43. $this->checkNotificationsId[$namespace] = array('namespaceName' => $namespace, 'notificationId' => $configValues['params']['notification'][$namespace]);
  44. }
  45. unset($configValues);
  46. }
  47. }
  48. }
  49. }
  50. public function setCluster($cluster)
  51. {
  52. $this->cluster = $cluster;
  53. }
  54. public function setClientIp($ip)
  55. {
  56. $this->clientIp = $ip;
  57. }
  58. public function setPullTimeout($pullTimeout) {
  59. $pullTimeout = intval($pullTimeout);
  60. if ($pullTimeout < 1 || $pullTimeout > 300) {
  61. return;
  62. }
  63. $this->pullTimeout = $pullTimeout;
  64. }
  65. public function setIntervalTimeout($intervalTimeout) {
  66. $intervalTimeout = intval($intervalTimeout);
  67. if ($intervalTimeout < 1 || $intervalTimeout > 300) {
  68. return;
  69. }
  70. $this->intervalTimeout = $intervalTimeout;
  71. }
  72. private function _getReleaseKey($config_file) {
  73. $releaseKey = '';
  74. if (file_exists($config_file)) {
  75. $last_config = require $config_file;
  76. is_array($last_config) && isset($last_config['releaseKey']) && $releaseKey = $last_config['releaseKey'];
  77. }
  78. return $releaseKey;
  79. }
  80. //获取单个namespace的配置文件路径
  81. public function getConfigFile($cluster) {
  82. return $this->save_dir.DIRECTORY_SEPARATOR.'main.'.$cluster.'.php';
  83. }
  84. //获取单个namespace/集群文件名
  85. public function getConfigFileNameSpace($cluster,$nameSpace,$public=0){
  86. $fileFirstName='';
  87. $nameSpace=substr($nameSpace, 0,strrpos($nameSpace, '.'));
  88. if($nameSpace=='application'){
  89. $fileFirstName='main';
  90. }else{
  91. $fileFirstName=$nameSpace;
  92. }
  93. if($cluster=='default'){
  94. $cluster='production';
  95. }
  96. if(!$public){
  97. return $this->save_dir.DIRECTORY_SEPARATOR.$fileFirstName.'.'.$cluster.'.php';
  98. }else{
  99. return $fileFirstName.'.'.$cluster.'.php';
  100. }
  101. }
  102. //获取单个namespace的配置-无缓存的方式
  103. public function pullConfig($namespaceName) {
  104. $base_api = rtrim($this->configServer, '/').'/configs/'.$this->appId.'/'.$this->cluster.'/';
  105. $api = $base_api.$namespaceName;
  106. $args = array();
  107. $args['ip'] = $this->clientIp;
  108. $config_file = $this->getConfigFile($this->cluster);
  109. $args['releaseKey'] = $this->_getReleaseKey($config_file);
  110. $api .= '?' . http_build_query($args);
  111. $ch = curl_init($api);
  112. curl_setopt($ch, CURLOPT_TIMEOUT, $this->pullTimeout);
  113. curl_setopt($ch, CURLOPT_HEADER, false);
  114. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  115. $body = curl_exec($ch);
  116. $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  117. $error = curl_error($ch);
  118. curl_close($ch);
  119. if ($httpCode == 200) {
  120. $result = json_decode($body, true);
  121. $content = '<?php return ' . var_export($result, true) . ';';
  122. file_put_contents($config_file, $content);
  123. }elseif ($httpCode != 304) {
  124. echo $body ?: $error."\n";
  125. return false;
  126. }
  127. return true;
  128. }
  129. //获取多个namespace的配置-无缓存的方式
  130. public function pullConfigBatch(array $namespaceNames) {
  131. if (! $namespaceNames) return array();
  132. $multi_ch = curl_multi_init();
  133. $request_list = array();
  134. $base_url = rtrim($this->configServer, '/').'/configs/'.$this->appId.'/'.$this->cluster.'/';
  135. $query_args = array();
  136. $query_args['ip'] = $this->clientIp;
  137. foreach ($namespaceNames as $namespaceName=>$notificationId) {
  138. $request = array();
  139. $request_url = $base_url.$namespaceName;
  140. // $query_args['releaseKey'] = $this->_getReleaseKey($config_file);
  141. // print_r($query_args['releaseKey']);exit;
  142. $query_string = '?'.http_build_query($query_args);
  143. $request_url .= $query_string;
  144. $ch = curl_init($request_url);
  145. curl_setopt($ch, CURLOPT_TIMEOUT, $this->pullTimeout);
  146. curl_setopt($ch, CURLOPT_HEADER, false);
  147. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  148. $request['ch'] = $ch;
  149. $request['notificationId'] = $notificationId;
  150. $request_list[$namespaceName] = $request;
  151. curl_multi_add_handle($multi_ch, $ch);
  152. }
  153. $active = null;
  154. // 执行批处理句柄
  155. do {
  156. $mrc = curl_multi_exec($multi_ch, $active);
  157. } while ($mrc == CURLM_CALL_MULTI_PERFORM);
  158. while ($active && $mrc == CURLM_OK) {
  159. if (curl_multi_select($multi_ch) == -1) {
  160. usleep(100);
  161. }
  162. do {
  163. $mrc = curl_multi_exec($multi_ch, $active);
  164. } while ($mrc == CURLM_CALL_MULTI_PERFORM);
  165. }
  166. // 获取结果
  167. $response_list = array();
  168. foreach ($request_list as $namespaceName => $req) {
  169. $response_list[$namespaceName] = true;
  170. $result = curl_multi_getcontent($req['ch']);
  171. $code = curl_getinfo($req['ch'], CURLINFO_HTTP_CODE);
  172. $error = curl_error($req['ch']);
  173. curl_multi_remove_handle($multi_ch,$req['ch']);
  174. curl_close($req['ch']);
  175. if ($code == 200) {
  176. $result = json_decode($result,true);
  177. $result=$this->YamalParseConfigData($result,$namespaceName,$req);
  178. $configFile=$this->getConfigFileNameSpace($this->cluster,$namespaceName);
  179. file_put_contents($configFile, $result);
  180. }elseif ($code != 304) {
  181. echo 'pull config of namespace['.$namespaceName.'] error:'.($result ?: $error)."\n";
  182. $response_list[$namespaceName] = false;
  183. }
  184. }
  185. curl_multi_close($multi_ch);
  186. return $response_list;
  187. }
  188. //格式化处理配置参数,合并公用配置
  189. protected function formatConfigData($result,$namespaceName){
  190. if(!$result || !is_array($result)) return null;
  191. $config=array();
  192. $publicConfig= require $this->publicConfigDir.$this->getConfigFileNameSpace($this->cluster,$namespaceName,1);
  193. foreach ($result['configurations'] as $key => $value){
  194. if(strpos($key,'.')!==false){
  195. $keyArr=explode('.',$key);
  196. if($keyArr){
  197. $realVal=json_decode($value,true)?json_decode($value,true):$value;
  198. $keyStr='$config';
  199. for($i=0;$i<count($keyArr);$i++){
  200. $keyStr.="['".$keyArr[$i]."']";
  201. }
  202. $keyStr.='=$realVal;';
  203. eval($keyStr);
  204. }
  205. }else{
  206. $realVal=json_decode($value,true)?json_decode($value,true):$value;
  207. $config[$key]=$realVal;
  208. }
  209. }
  210. $config=array_merge_recursive($publicConfig,$config);
  211. return $config;
  212. }
  213. //格式化处理配置参数,合并公用配置
  214. protected function YamalParseConfigData($result,$namespaceName,$req){
  215. if(!$result || !is_array($result)) return null;
  216. $content='';
  217. if(isset($result['configurations']) && isset($result['configurations']['content']) && $result['configurations']['content']){
  218. $config=yaml_parse($result['configurations']['content']);
  219. $replaceArr=$this->recur($config);
  220. if(strpos($namespaceName,'qcloud')!==false && isset($config['string'])){
  221. $content=" <?php \n ".implode("\n",$config['string'])." ";
  222. }else{
  223. $config['params']['notification'][$namespaceName]=$req['notificationId'];
  224. $content=" <?php return ".var_export($config,true)." ;";
  225. if($replaceArr){
  226. $content=str_replace(array_keys($replaceArr),array_values($replaceArr),$content);
  227. }
  228. }
  229. }
  230. return $content;
  231. }
  232. protected function _listenChange(&$ch) {
  233. $base_url = rtrim($this->configServer, '/').'/notifications/v2?';
  234. $params = array();
  235. $params['appId'] = $this->appId;
  236. $params['cluster'] = $this->cluster;
  237. $params['notifications'] = json_encode(array_values($this->notifications));
  238. $query = http_build_query($params);
  239. curl_setopt($ch, CURLOPT_URL, $base_url.$query);
  240. $response = curl_exec($ch);
  241. $httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
  242. $error = curl_error($ch);
  243. if ($httpCode == 200) {
  244. $res = json_decode($response, true);
  245. $change_list=array();
  246. foreach ($res as $r) {
  247. if (!isset($this->checkNotificationsId[$r['namespaceName']]) || $r['notificationId'] != $this->checkNotificationsId[$r['namespaceName']]['notificationId']) {
  248. $change_list[$r['namespaceName']] = $r['notificationId'];
  249. }
  250. }
  251. if($change_list){
  252. $response_list= $this->pullConfigBatch($change_list);
  253. foreach ($response_list as $name => $value){
  254. if(!$value){
  255. echo $name.':同步配置失败'.PHP_EOL;
  256. }else{
  257. echo $name.':同步配置成功'.PHP_EOL;
  258. }
  259. }
  260. }
  261. }elseif ($httpCode != 304) {
  262. throw new \Exception($response ?: $error);
  263. }
  264. }
  265. /**
  266. * @param $callback 监听到配置变更时的回调处理
  267. * @return mixed
  268. */
  269. public function start() {
  270. $ch = curl_init();
  271. curl_setopt($ch, CURLOPT_TIMEOUT, $this->intervalTimeout);
  272. curl_setopt($ch, CURLOPT_HEADER, false);
  273. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  274. try {
  275. $this->_listenChange($ch);
  276. }catch (\Exception $e) {
  277. curl_close($ch);
  278. return $e->getMessage();
  279. }
  280. }
  281. public function setReplaceArr($arr){
  282. $this->replaceValueArr=$arr;
  283. }
  284. //查找需要替换的变量
  285. function recur($array){
  286. $data = array();
  287. array_walk_recursive($array, function ($v, $k) use (&$data) {
  288. if(strpos($v,'////')!==false){
  289. $data["'".$v."'"]=str_replace('////','',$v);
  290. }
  291. if(strpos($k,'////')!==false){
  292. $data["'".$k."'"]=str_replace('////','',$k);
  293. }
  294. });
  295. return $data;
  296. }
  297. }