Skip to content

Commit 9a13f23

Browse files
committed
v2.0.0 ClickHouse cluster support
1 parent 884b21f commit 9a13f23

20 files changed

+632
-66
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 2.0.0 [2024-07-26]
2+
3+
### Features
4+
1. ClickHouse cluster support
5+
6+
### Breaking changes
7+
1. The minimum required PHP version is 8.0
8+
2. Removed deprecated method BaseModel::insert, use BaseModel::insertBulk instead
9+
3. Method BaseModel::prepareAndInsert is marked as deprecated, use BaseModel::prepareAndInsertBulk instead
10+
111
## 1.19.0 [2023-09-28]
212

313
### Features

README.md

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ Adapter to Laravel and Lumen of the most popular libraries:
99

1010
## Features
1111

12-
No dependency, only Curl (support php >=7.1 )
12+
No dependency, only Curl (support php >=8.0 )
1313

1414
More: https://github.com/smi2/phpClickHouse#features
1515

1616
## Prerequisites
1717

18-
- PHP 7.1, 8.0
18+
- PHP 8.0
1919
- Laravel/Lumen 7+
2020
- Clickhouse server
2121

@@ -87,7 +87,6 @@ More about `$db` see here: https://github.com/smi2/phpClickHouse/blob/master/REA
8787
```php
8888
<?php
8989

90-
9190
namespace App\Models\Clickhouse;
9291

9392
use PhpClickHouseLaravel\BaseModel;
@@ -105,7 +104,6 @@ class MyTable extends BaseModel
105104
```php
106105
<?php
107106

108-
109107
class CreateMyTable extends \PhpClickHouseLaravel\Migration
110108
{
111109
/**
@@ -354,7 +352,6 @@ MyTable::insertAssoc([[1, 'str', new InsertArray(['a','b'])]]);
354352
```php
355353
<?php
356354

357-
358355
namespace App\Models\Clickhouse;
359356

360357
use PhpClickHouseLaravel\BaseModel;
@@ -386,3 +383,85 @@ return new class extends \PhpClickHouseLaravel\Migration
386383
}
387384
};
388385
```
386+
387+
### Cluster mode
388+
389+
**Important!**
390+
* Each ClickHouse node must have one database name and login and password.
391+
* For reading and writing, the connection is made to the first available node.
392+
* Migrations executes on all nodes. If one of the nodes is unavailable, the migration will throw an exception.
393+
394+
Your config/database.php should look like:
395+
```php
396+
'clickhouse' => [
397+
'driver' => 'clickhouse',
398+
'cluster' => [
399+
[
400+
'host' => 'clickhouse01',
401+
'port' => '8123',
402+
],
403+
[
404+
'host' => 'clickhouse02',
405+
'port' => '8123',
406+
],
407+
],
408+
'database' => env('CLICKHOUSE_DATABASE','default'),
409+
'username' => env('CLICKHOUSE_USERNAME','default'),
410+
'password' => env('CLICKHOUSE_PASSWORD',''),
411+
'timeout_connect' => env('CLICKHOUSE_TIMEOUT_CONNECT',2),
412+
'timeout_query' => env('CLICKHOUSE_TIMEOUT_QUERY',2),
413+
'https' => (bool)env('CLICKHOUSE_HTTPS', null),
414+
'retries' => env('CLICKHOUSE_RETRIES', 0),
415+
'settings' => [ // optional
416+
'max_partitions_per_insert_block' => 300,
417+
],
418+
],
419+
```
420+
421+
Migration is:
422+
423+
```php
424+
<?php
425+
426+
return new class extends \PhpClickHouseLaravel\Migration
427+
{
428+
/**
429+
* Run the migrations.
430+
*
431+
* @return void
432+
*/
433+
public function up()
434+
{
435+
static::write('
436+
CREATE TABLE my_table (
437+
id UInt32,
438+
created_at DateTime,
439+
field_one String,
440+
field_two Int32
441+
)
442+
ENGINE = ReplicatedMergeTree('/clickhouse/tables/default.my_table', '{replica}')
443+
ORDER BY (id)
444+
');
445+
}
446+
447+
/**
448+
* Reverse the migrations.
449+
*
450+
* @return void
451+
*/
452+
public function down()
453+
{
454+
static::write('DROP TABLE my_table');
455+
}
456+
};
457+
```
458+
459+
You can get the host of the current node and switch the active connection to the next node:
460+
```php
461+
$row = new MyTable();
462+
echo $row->getThisClient()->getConnectHost();
463+
// will print 'clickhouse01'
464+
$row->resolveConnection()->getCluster()->slideNode();
465+
echo $row->getThisClient()->getConnectHost();
466+
// will print 'clickhouse02'
467+
```

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
}
1818
],
1919
"require": {
20-
"php": ">=7.1.0|>=8.0",
20+
"php": ">=8.0",
2121
"smi2/phpclickhouse": "^1.4.2",
2222
"the-tinderbox/clickhouse-builder": "^6.0",
2323
"illuminate/support": ">=7",

docker-compose.test.yaml

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,29 @@ services:
55
php:
66
build: tests/docker
77
depends_on:
8-
- clickhouse
9-
- clickhouse2
8+
- clickhouse01
9+
- clickhouse02
10+
- zookeeper
1011
volumes:
1112
- ./:/src
1213

13-
clickhouse:
14+
clickhouse01:
1415
image: yandex/clickhouse-server
15-
ulimits:
16-
nproc: 65535
17-
nofile:
18-
soft: 262144
19-
hard: 262144
2016
ports:
2117
- "18123:8123"
18+
depends_on:
19+
- zookeeper
20+
volumes:
21+
- ./tests/docker/clickhouse01:/etc/clickhouse-server
2222

23-
clickhouse2:
23+
clickhouse02:
2424
image: yandex/clickhouse-server
25-
ulimits:
26-
nproc: 65535
27-
nofile:
28-
soft: 262144
29-
hard: 262144
25+
ports:
26+
- "18124:8123"
27+
depends_on:
28+
- zookeeper
29+
volumes:
30+
- ./tests/docker/clickhouse02:/etc/clickhouse-server
31+
32+
zookeeper:
33+
image: zookeeper:3.7

src/BaseModel.php

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ class BaseModel
7272
* @var string
7373
*/
7474
protected $connection = Connection::DEFAULT_NAME;
75-
75+
7676
/**
7777
* Determine if an attribute or relation exists on the model.
7878
* The __isset magic method is triggered by calling isset() or empty() on inaccessible properties.
7979
*
80-
* @param string $key The name of the attribute or relation.
80+
* @param string $key The name of the attribute or relation.
8181
* @return bool True if the attribute or relation exists, false otherwise.
8282
*/
8383
public function __isset($key)
@@ -97,7 +97,7 @@ public function __isset($key)
9797

9898
return false;
9999
}
100-
100+
101101
/**
102102
* Get the table associated with the model.
103103
*
@@ -190,18 +190,6 @@ public function save(array $options = []): bool
190190
return $this->exists;
191191
}
192192

193-
/**
194-
* Bulk insert into Clickhouse database
195-
* @param array[] $rows
196-
* @return Statement
197-
* @deprecated use insertBulk
198-
*/
199-
public static function insert(array $rows): Statement
200-
{
201-
$instance = new static();
202-
return $instance->getThisClient()->insert($instance->getTableForInserts(), $rows);
203-
}
204-
205193
/**
206194
* Bulk insert into Clickhouse database
207195
* @param array[] $rows
@@ -232,6 +220,21 @@ public static function insertBulk(array $rows, array $columns = []): Statement
232220
* @param array $columns
233221
* @return Statement
234222
*/
223+
public static function prepareAndInsertBulk(array $rows, array $columns = []): Statement
224+
{
225+
return static::insertBulk(
226+
array_map('static::prepareFromRequest', $rows, $columns),
227+
$columns
228+
);
229+
}
230+
231+
/**
232+
* Prepare each row by calling static::prepareFromRequest to bulk insert into database
233+
* @param array[] $rows
234+
* @param array $columns
235+
* @return Statement
236+
* @deprecated use prepareAndInsertBulk
237+
*/
235238
public static function prepareAndInsert(array $rows, array $columns = []): Statement
236239
{
237240
$rows = array_map('static::prepareFromRequest', $rows, $columns);
@@ -319,9 +322,6 @@ public static function select($select = ['*']): Builder
319322
return $instance->newQuery()->select($select)->from($instance->getTable());
320323
}
321324

322-
/**
323-
* @return Builder
324-
*/
325325
protected function newQuery(): Builder
326326
{
327327
return new Builder($this->getThisClient());

src/Cluster.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace PhpClickHouseLaravel;
4+
5+
use ClickHouseDB\Client;
6+
use ClickHouseDB\Exception\TransportException;
7+
use ClickHouseDB\Statement;
8+
9+
class Cluster
10+
{
11+
/**
12+
* @var Client[]
13+
*/
14+
protected array $nodes;
15+
protected int $activeNodeIndex;
16+
17+
public function __construct(
18+
protected array $nodeConfigs
19+
) {
20+
foreach ($this->nodeConfigs as $index => $nodeConfig) {
21+
try {
22+
$this->nodes[$index] = static::createClient($nodeConfig);
23+
$this->nodes[$index]->ping(true);
24+
$this->activeNodeIndex = $index;
25+
break;
26+
} catch (TransportException $e) {
27+
}
28+
}
29+
if (!isset($this->activeNodeIndex)) {
30+
throw $e ?? new TransportException('No nodes are available');
31+
}
32+
}
33+
34+
public function write(string $sql, array $bindings = [], bool $exception = true): ?Statement
35+
{
36+
foreach ($this->nodeConfigs as $index => $config) {
37+
if (empty($this->nodes[$index])) {
38+
$this->nodes[$index] = static::createClient($config);
39+
}
40+
$statement = $this->nodes[$index]->write($sql, $bindings, $exception);
41+
}
42+
return $statement ?? null;
43+
}
44+
45+
public function getActiveNode(): Client
46+
{
47+
return $this->nodes[$this->activeNodeIndex];
48+
}
49+
50+
/**
51+
* Switch active node to the next available node
52+
* @return void
53+
*/
54+
public function slideNode(): void
55+
{
56+
$configCount = count($this->nodeConfigs);
57+
if ($configCount < 2) {
58+
return;
59+
}
60+
for ($i = 0; $i < $configCount; $i++) {
61+
$nextIndex = $this->activeNodeIndex + 1;
62+
if ($configCount == $nextIndex) {
63+
$nextIndex = 0;
64+
}
65+
try {
66+
$this->nodes[$nextIndex] ??= static::createClient($this->nodeConfigs[$nextIndex]);
67+
$this->nodes[$nextIndex]->ping(true);
68+
$this->activeNodeIndex = $nextIndex;
69+
break;
70+
} catch (TransportException) {
71+
}
72+
}
73+
}
74+
75+
protected static function createClient(array $config): Client
76+
{
77+
$client = new Client($config);
78+
$client->database($config['database']);
79+
$client->setTimeout((int)$config['timeout_query']);
80+
$client->setConnectTimeOut((int)$config['timeout_connect']);
81+
if ($configSettings =& $config['settings']) {
82+
$settings = $client->settings();
83+
foreach ($configSettings as $sName => $sValue) {
84+
$settings->set($sName, $sValue);
85+
}
86+
}
87+
if ($retries = (int)($config['retries'] ?? null)) {
88+
$curler = new CurlerRollingWithRetries();
89+
$curler->setRetries($retries);
90+
$client->transport()->setDirtyCurler($curler);
91+
}
92+
return $client;
93+
}
94+
}

0 commit comments

Comments
 (0)