一句话看懂集群、微服务和分布式的区别
发表于|更新于|码不能停
|总字数:99|阅读时长:1分钟|浏览量:
一个完整的服务拆分成多个微小的服务,就是微服务。
每个服务不止一个就是集群。
微服务部署在多台服务器上,服务器之间可以相互通信就是分布式。
微服务与分布式的细微差别是微服务可以部署在一台服务器,也可以部署在多台服务器。
文章作者: m-finder
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 M-finder!
相关推荐

2025-12-10
飞牛 NAS + Tailscale 组网踩坑实录
废废的话多多的说家里有台用了很多年的笔记本, 型号是联想小新 700, i7 的 CPU, 8G 内存, 硬盘是买来时我自己换的一块固态 256. 这台笔记本购于 17 年 10 月, 陪我度过了刚毕业时的懵懂, 也陪大美丽度过了研究僧苦修的日常. 进可敲代码, 退可搞学术, 进过水, 也换过屏, 可以说是功高劳苦, 遍体鳞伤. 不忍心让它就这么丢在家里吃灰, 前段时间我一个没忍住, 买了一块 2T 的机械, 一块 1T 的固态, 还有一根 16G 的内存条. 换上以后, 大黑满血复活. 当时有个老登整天找我念叨要组装小主机, 搞 NAS, 我被折磨久了, 心里也有火苗在跳. 心动完我先整理了一下需求, 然后丢给 AI 帮我整理方案: 我有一台闲置的联想小新700电竞版笔记本,i7的cpu,16+8g内存,GTX905m显卡,1t固态硬盘,2t机械硬盘,想安装飞牛OS作为影音nas,win10用来打steam游戏,linux和macOS用来写代码开发php/java/qt/swift,现在了解到可以通过hyper-v, vm等虚拟机,也可以通过pve,ESXi等虚拟化平台满足我的需求,我对开发和游戏可能有一定的性能要求,帮我分析下用哪种方案最优 AI 说我这个需求 PVE 是首选. 那还说啥, 搞呗. 趁着周末, 我一口气折腾到半夜, 然后发现 GG 了. 大黑不支持一个什么 Bios 直通, 给 Bios 升完级依然找不到对应选项. 我特么心里哇凉啊, 头发咔咔掉一地. 裤子都脱了就给我看这个? 第二天一早我还是老老实实地装了 Windows, 然后又通过 Hyper-V 装了飞牛 NAS. 我给飞牛绑了局域网内的固定 IP, 然后把 2T 的机械盘从 windows 禁用掉, 专门给飞牛做存储, 最后又通过 SMB 把硬盘挂载回了大黑, 方便我在不开浏览器的时候也能随时查看飞牛里的文件. 说实话, 用着是真的爽啊, 自带的远程连接虽然只有 2M 带宽, 但是也足够我在公司上传下载文件用, 比之前玩黑群晖的体验强到没话说! 玩了几天以后, 我又喊上老登一块儿联机我的世界, 当时龙猫的启动器正在小范围试用联机功能, 老登刚好就有, 分享完我们畅玩了一天. 结果第二天就看到龙猫发了篇声明, 联机功能没了, 组网的需求嗷一下就上来了. 鉴于龙猫用的 EasyTier, 我也先尝试了一下, 发现功能确实好用, 但碍于我不喜欢他们那套 UI, 我又放弃了. 没错, 颜控就是这么离谱. 挑来选去, 最后还是选了 Tailscale. 踩坑过程虽然 Tailscale 下载安装后登录下账号就能用, 但是最开始我没搞明白, 也不知道它其实需要客户端和命令行结合使用. 我只发现两台设备都安装登录后, 互相访问似乎是没什么反应. 拉上老登又陪我折腾一圈, 发现登录完设备之间确实是通了, 我当时不通是因为访问了飞牛的内网 IP, 而我, 跟本就没给它配置子网路由. 😭 首先, Windows 安装后, 可以直接在命令行使用 tailscale 的各项命令, Mac 安装后, 需要手动添加一个环境变量: 1export PATH="$PATH:/Applications/Tailscale.app/Contents/MacOS" 然后才能在命令行愉快地玩耍, 比如通过 tailscale status 查看各台设备的连接状态, 再或者通过 tailscale ping ip 来查看当前设备到对应设备的连通状态. 也就是这个时候, 我和老登都发现设备之间虽然通了, 但是延迟有点高. AI 说, 应该是没能打洞成功, 再一看 status 的结果, 果然都是中转. 它让我先把 41641 端口在防火墙放开, 但是我没开防火墙, emmm🤔… 那问题就只剩一个, 网络层给我拦了! 登上光猫后台, 没找到什么 UPnP, 但是有个端口映射, 我给配了一下, 再运行 tailscale status, 连接状态果然变成了直连, 快多了. 当天晚上又拉老登联机了一个游戏, 结果游戏内时不时弹起的网络延迟过高让我有点蛋疼. 打开测速网一跑: 73 Mbs, 我千兆的光纤 FTTR, 可以说是拉了一裤兜. 把 Tailscale 关了, 再跑能到 230, 大黑的无线网卡是 400 多, 也算能说得过去. 初步怀疑运营商对大端口的 UDP 做了主动丢包, 尝试查找怎么给 Tailscale 配个端口, 强制所有流量去走 443, 安装目录添加配置C:\ProgramData\Tailscale\config.json: 12345{ "ForceTCP": true, "Socks5Server": "localhost:443", "TunMode": "userspace"} 配置后重启,似乎没啥作用. 微软服务内添加 --port=443 后重启, 依然没啥作用. 正跟 AI 较劲到底该怎么配的时候, 我突然看到之前配置子网路由时的命令, 似乎带了一句 --accept-routes, 去掉以后再测速, 又回到 200+. 总结, 不知道是哪个傻逼在博客里提了一嘴改 443, 被 AI 抓取到以为 Tailscale 是可以配端口的, 特么误我啊! 已知问题如果是 Mac 且开了代理, 打开文件管理有很大概率一直转圈, 似乎是因为 Safari 在代理模式有概率连不上 Socket. 解决方法未知, 暂时只能换浏览器或者关代理.

2025-01-22
mac m1 安装 swoole 报错问题记录
今天切本地环境的时候发现 swoole 还没装,装的时候又报了错:pcre2.h file not found,鉴于我这个记性越来越差,还是写条笔记记录一下解决办法。 brew install pcre2,如果本机已经安装,就直接进行第二步 ln -s /opt/homebrew/include/pcre2.h /opt/homebrew/Cellar/php@8.1/8.1.31/include/php/ext/pcre/pcre2.h,正常安装 pcre2 之后,需要软连接对应目录,注意这里要把目录切换成你实际的目录 pecl install swoole,再次运行安装 php -m | grep swoole,运行验证

2025-02-13
follow 认证
This message is used to verify that this feed (feedId:104434909303929856) belongs to me (userId:68900854292651008). Join me in enjoying the next generation information browser https://follow.is. ps. 需要邀请码的可以联系.

2025-09-12
Mac 菜单栏多合一工具 FancyTool 更新啦!
本次更新聚焦「轻量体验」深度优化:不仅重构了 CPU 占用逻辑与系统唤醒机制,让后台运行更高效;更让动画交互全程保持丝滑流畅,资源消耗却低到近乎无感 —— 哪怕它常驻菜单栏,你也几乎察觉不到它的存在,既不拖慢系统,又能随时响应需求~ 下载地址: [ github ] [ gitee ] 目前软件主要包含以下功能: 🚀 智能CPU动态图标 让性能可视化 将任何GIF图片设置为你的菜单栏图标,它的播放速度会实时响应你的CPU使用率 空闲时悠然自得,高负荷时急速狂飙,用最酷的方式监控系统状态 支持完全自定义上传,打造你的专属动画 🌈 渐变彩色心情签名 用美丽的渐变色彩表达每日心情状态 完全可自定义的颜色和文字,展现独特个性 为您的菜单栏增添一抹艺术气息 📋 高效剪切板管理 记录多次复制历史,随时找回需要的内容 智能分类整理,快速定位所需片段 支持文本、图片等多种格式,提高工作效率 🖥️ 屏幕圆角美化 为Mac屏幕添加优雅圆角,提升视觉美感 智能适配多显示器设置,每块屏幕完美呈现 无性能影响的背景运行,细腻改善视觉体验 📎 菜单栏折叠工具 自动整理拥挤的菜单栏图标,保持界面整洁 一键展开/折叠,平衡简洁与便捷 自定义排序和分组,完全按您的方式组织 技术特点 原生 Swift 开发,完美兼容最新系统 轻量级设计,资源占用极低 直观易用的界面,无需学习成本 定期更新,持续改进功能和体验 如果有好的建议或者使用时遇到任何问题欢迎随时反馈👏

2023-11-20
为你的 Laravel 应用添加一个基于 Swoole 的 WebSocket 服务
做了一个基于 Swoole 的 WebSocket 扩展包,可以用来做实时状态推送,或者自定义消息处理实现 im,有需要的可以看看: [giorgio-socket] 使用方法安装安装扩展包 1composer require wu/giorgio-socket 发布配置文件 1php artisan vendor:publish --provider="GiorgioSocket\Providers\SocketServiceProvider" 运行 Socket 服务 1php artisan socket:start 注意事项 可以通过实现 GiorgioSocket\Services\Handlers\Interfaces 下的接口类来自定义自己的业务逻辑。 如果要从服务端发送消息,这里有两种方式: 第一种,借助 Laravel HTTP 客户端 123456Route::get('/socket', function () { \Illuminate\Support\Facades\Http::asForm()->post('http://127.0.0.1:9501', [ 'to' => 2, 'message' => 'server message', ]);}); 第二种:借助 Laravel Listener,需要将 .env 文件中的 QUEUE_CONNECTION 配置修改为 redis 或其他异步队列。配置更改后,运行以下命令:php-artisan queue:work --queue=socket-listener监听队列,然后按以下代码调用 event: 123Route::any('socket', function (Request $request){ \GiorgioSocket\Events\SocketEvent::dispatch($request->get('to'), $request->get('message'));}); 如果你正在使用 laravel/breeze 扩展包,并且使用了 Blade 模板,可以将以下代码粘贴到 dashboard.blade.php 中进行快速测试: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111@auth <div class="py-12"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg"> <div class="grid grid-cols-1 md:grid-cols-2"> <div class="p-6" id="server-message"> messages:<br/> </div> <div class="p-6"> <label class="block font-medium text-sm text-gray-700 dark:text-gray-300" for="from">from</label> <input class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm block mt-1 w-full" value="{{ auth()->user()->getKey() }}" id="from"> <label class="block font-medium text-sm text-gray-700 dark:text-gray-300" for="to">to</label> <input class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm block mt-1 w-full" value="" id="to"> <label class="block font-medium text-sm text-gray-700 dark:text-gray-300" for="message">message</label> <textarea class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm block mt-1 w-full" id="message"></textarea> <input class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 mt-3" type="button" id="submit" value="submit"> </div> </div> </div> </div> </div> <script type="text/javascript"> let heartBeatTimer = 0; let socket = connectWebSocket(); function startHeartbeat(interval) { interval = interval || 30; heartBeatTimer = setInterval(function () { sendMessage(null, "heart_beat"); }, interval * 1000); } function stopHeartbeat() { clearInterval(heartBeatTimer); } function connectWebSocket() { const wsServer = 'ws://127.0.0.1:9501'; const socket = new WebSocket(wsServer); let userId = document.getElementById('from').value; socket.onopen = function (evt) { let data = { user_id: userId, type: 'connect' }; console.log('open', data) socket.send(JSON.stringify(data)); }; socket.onmessage = function (evt) { console.log('get message from server: ' + evt.data); if (evt.data !== 'heart_beat') { let data = JSON.parse(evt.data); let message = document.getElementById("server-message") message.innerHTML += data.user_name + ': ' + data.data + '<br/>' } }; socket.onerror = function (evt) { console.log(evt); }; socket.onclose = function () { let data = { user_id: userId, type: 'close' }; socket.send(JSON.stringify(data)); }; return socket; } function sendMessage(to, message) { if (socket != null && socket.readyState === WebSocket.OPEN) { if (message !== 'heart_beat') { let messageBox = document.getElementById("server-message") messageBox.innerHTML += 'me: ' + message + '<br/>' } let from = document.getElementById("from") socket.send(JSON.stringify({ user_id: from.value, user_name: '{{ auth()->user()->name }}', to: to, type: 'message', data: message, })); console.log("webSocket send message:" + JSON.stringify({ user_id: from.value, user_name: '{{ auth()->user()->name }}', to: to, type: 'message', data: message, })); } else { console.log("webSocket closed"); } } let button = document.getElementById("submit"); button.addEventListener('click', function () { let message = document.getElementById("message"); let to = document.getElementById("to"); sendMessage(to.value, message.value) }); </script>@endauth 如有任何疑问,欢迎提交 [issue]

2024-02-28
Laravel Octane 和 Swoole 协程的使用分析
之前在工作中使用 Laravel Octane 的 concurrently 处理并发时,发现在队列和定时任务中不会触发并发效果。经过分析,作了如下猜测:队列定时任务都属于一个独立的进程,与 Octane 服务无关,而 Octane conturrently 恰恰需要在 Octane 环境下才能运行。 后来通过代码进行环境检测和查看 php 的进程,证明猜想成立。 1234info('check env', [ 'served by octane' => isset($_SERVER['LARAVEL_OCTANE']) && ((int)$_SERVER['LARAVEL_OCTANE'] === 1), 'on swoole server' => (extension_loaded('swoole') || extension_loaded('openswoole')) && app()->bound(Server::class)]); 为了能够在任意代码中实现并发,我们研究参考了 Hyperf 框架关于协程的代码,然后抽取了如下两个类: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071<?phpnamespace App\Services;use App\Exceptions\ParallelExecutionException;use Laravel\Octane\Facades\Octane;use Throwable;use Swoole\Coroutine as Co;class Parallel{ protected array $callbacks = []; protected array $results = []; /** * @var Throwable[] */ protected array $throwables = []; public function add(callable $callable, $key = null): void { if (is_null($key)) { $this->callbacks[] = $callable; } else { $this->callbacks[$key] = $callable; } } public function wait(bool $throw = true): array { if (isset($_SERVER['LARAVEL_OCTANE']) && ((int)$_SERVER['LARAVEL_OCTANE'] === 1)) { return Octane::concurrently($this->callbacks, 300000); } app('log')->useLoggingLoopDetection(false); Co\run(function () { foreach ($this->callbacks as $key => $callback) { Co::create(function () use ($callback, $key) { try { $this->results[$key] = $callback(); } catch (Throwable $throwable) { $this->throwables[$key] = $throwable; unset($this->results[$key]); } }); } }); if ($throw && ($throwableCount = count($this->throwables)) > 0) { $message = 'Detecting ' . $throwableCount . ' throwable occurred during parallel execution:' . PHP_EOL . $this->formatThrowAbles($this->throwables); $executionException = new ParallelExecutionException($message); $executionException->setResults($this->results); $executionException->setThrowAbles($this->throwables); unset($this->results, $this->throwables); throw $executionException; } app('log')->useLoggingLoopDetection(true); return $this->results; } private function formatThrowAbles(array $throwables): string { $output = ''; foreach ($throwables as $key => $value) { $output .= sprintf('(%s) %s: %s' . PHP_EOL . '%s' . PHP_EOL, $key, get_class($value), $value->getMessage(), $value->getTraceAsString()); } return $output; }} 1234567891011121314151617181920212223242526272829303132<?phpnamespace App\Exceptions;use RuntimeException;class ParallelExecutionException extends RuntimeException{ protected array $results = []; protected array $throwables = []; public function getResults(): array { return $this->results; } public function setResults(array $results): void { $this->results = $results; } public function getThrowAbles(): array { return $this->throwables; } public function setThrowAbles(array $throwables): array { return $this->throwables = $throwables; }} 其中,第一个类的作用是检测系统是否运行在 Octane 环境下,是则调用Octane concurrently,否则就执行 Swoole 协程代码,使用起来也比较简单: 1234567$parallel = new Parallel();$parallel->add(fn() => $this->analysisStructure(), 'structure');$parallel->add(fn() => $this->analysisHabit(), 'habit');[ 'structure' => $structure, 'habit' => $habit,] = $parallel->wait(); 之所以没有完全使用 Swoole 协程,是因为相比之下,Octane 代码更加优雅,我们在期待着某天更新后,Octane concurrently 也能直接在队列中运行使用。 第二个类的作用比较简单,就是对协程中异常的一个定义。 另外在分析过程中,我们也发现了一个比较有意思的事情: 如图所示,当我在路由中运行检测代码时,Octane 和 Swoole server 都为 true;在控制器中运行检测代码时,又只有 Octane 为true;为什么会有这样的区分?我个人猜测是 Octane 在将框架代码读进内存时,特意跳过了控制器中的代码,以避免数据更新不及时等问题。 有知道具体原因的小伙伴,欢迎留言探讨。