Building a Search System with Cassandra, SolrCloud, Zookeeper, Kafka, and Python Microservices
This system design assumes a Linux infrastructure (CentOS or Ubuntu) with JDK installed, since Cassandra, Solr, and Kafka all require Java. Deployment uses multiple servers organized as follows:
- Zookeeper cluster: 3 nodes for coordinating Kafka and SolrCloud (IP range 10.201.X.X, client port 2181).
- Kafka cluster: 3 or more nodes, using the Zookeeper cluster for coordination. Kafka receives and distributes product data update messages.
- Cassandra: 1 or more nodes depending on data volume and fault tolerance requirements. Stores product data (single node at IP 10.201.X.X).
- SolrCloud: 3 Solr nodes forming a search index cluster (IP addresses 10.201.X.X across three machines).
- Python microservices:
- Data sync service: consumes Kafka messages, writes new product data to Cassandra, and updates Solr indices.
- Search API service: provides REST endpoints for the frontend, queries SolrCloud to return results (also enables access control and aggregation at the service layer).
All components communicate over internal networks with firewall rules allowing necessary ports (Cassandra 9042, Solr 8983, Zookeeper 2181, Kafka 9092, and microservice ports as configured). The examples below use placeholder IPs like 10.201.X.X; substitute your actual addresses in production.
Installing and Configuring Cassandra
Install Cassandra
Download Cassandra from the Apache site (this example uses 3.11.12), upload to the target server 10.201.X.X, and extract:
$ wget https://downloads.apache.org/cassandra/3.11.12/apache-cassandra-3.11.12-bin.tar.gz
$ tar -zxvf apache-cassandra-3.11.12-bin.tar.gz
$ cd apache-cassandra-3.11.12
Cassandra requires no compilation. To start the node, enter the Cassandra directory and run bin/cassandra. Before the first startup, modify the configuration to ensure node communication works correctly.
Configure Cassandra nodes
Edit conf/cassandra.yaml to match your network environment:
- cluster_name: a descriptive name for your cluster, e.g., cluster_name: "SearchCluster".
- listen_address: the node's internal IP, e.g., listen_address: 10.201.X.X.
- rpc_address: controls the address for CQL service binding. Set it to your node IP, e.g., rpc_address: 10.201.X.X (on Cassandra 3.x, rpc_address controls CQL).
- seed_provider: the list of seed nodes for cluster bootstrapping. For a single test node, set it to the node's own IP; for a multi-node cluster, list the initial seed node IPs:
seed_provider:
- class_name: org.apache.cassandra.locator.SimpleSeedProvider
parameters:
- seeds: "10.201.X.X,10.201.X.X"
At least one node IP in the cluster should be listed as a seed.
- Other parameters: adjust memory and garbage collection settings as needed. Default configuration works for most cases; in production, adjust -Xmx based on available memory.
Start Cassandra:
$ bin/cassandra -R # 后台启动 Cassandra (-R 可去除超级用户模式限制)
Initial startup may take a moment for initialization. Verify connectivity with the CQL shell:
$ bin/cqlsh 10.201.X.X 9042 # 连接到本机的 Cassandra CQL 服务
Connected to SearchCluster at 10.201.X.X:9042.
cqlsh>
Once connected, create a keyspace and tables. For example, create the search keyspace:
CREATE KEYSPACE search
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}
AND durable_writes = true;
Note: The above configuration uses simple replication strategy and replication factor 1, suitable only for testing. Production clusters should use NetworkTopologyStrategy and appropriate replication factors based on datacenter topology.
Design Cassandra tables for your search use case by organizing data around query patterns. Cassandra's strength lies in storing wide-row data with query-driven schema design. Create multiple tables in the search keyspace:
- Product table (goods): stores primary product fields with product ID as the partition key, including name, description, and category.
- Attribute tables: store product-specific attributes by type, with product ID as partition key and a version field to track updates.
- Dimensional tables (category, etc.): created as needed to support queries or analytics by category, brand, or promotion status.
For search workloads, Cassandra acts as the persistent store and data source. Solr handles the actual query execution via its indices, so Cassandra is typically not queried directly during search; instead, product data is pre-loaded into Solr by the indexing service.
Installing and Configuring Zookeeper
Zookeeper serves two roles in this architecture: coordination for Kafka and configuration management for SolrCloud. This example uses a 3-node cluster to ensure high availability.
Install Zookeeper
Download Zookeeper (e.g., version 3.7.0) and extract on each server:
$ wget https://downloads.apache.org/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz
$ tar -zxvf apache-zookeeper-3.7.0-bin.tar.gz
$ mv apache-zookeeper-3.7.0-bin /opt/app/zookeeper
Assume three Zookeeper servers with IPs 10.201.X.X, 10.201.X.X, and 10.201.X.X respectively.
Configure Zookeeper
Copy the example configuration on each node:
$ cp conf/zoo_sample.cfg conf/zoo.cfg
Edit zoo.cfg and configure the following:
- dataDir=/path/to/zookeeper/data: specifies the data directory, e.g., /opt/app/zookeeper/data. Ensure this directory exists with read and write permissions.
- clientPort=2181: the client connection port. Leave this at the default.
- Cluster configuration: add the following to the end of the file:
server.1=10.201.X.X:2888:3888
server.2=10.201.X.X:2888:3888
server.3=10.201.X.X:2888:3888
The N in server.N corresponds to each node's unique ID. Port 2888 is for cluster communication, and 3888 is for leader election.
- Create a file named myid in the dataDir on each node, containing only that node's ID (e.g., node 1 contains "1", node 2 contains "2", etc.).
Start the Zookeeper cluster
On each server, run:
$ ./bin/zkServer.sh start
After startup, check the status with zkServer.sh status. One node should be the leader, and others followers. To verify Zookeeper availability, connect with zkCli:
$ ./bin/zkCli.sh -server 10.201.X.X:2181
Test by creating a node:
[zk: 10.201.X.X:2181(CONNECTED) 0] create /solr_demo "test"
Successful creation confirms Zookeeper is operational. We will later store SolrCloud configuration data at /solr_demo in Zookeeper (a chroot path).
Installing and Configuring Kafka
Install Kafka
Download Kafka from the Kafka website or Apache archives (this example uses Kafka 2.x):
$ wget https://downloads.apache.org/kafka/2.7.0/kafka_2.12-2.7.0.tgz
$ tar -zxvf kafka_2.12-2.7.0.tgz
$ mv kafka_2.12-2.7.0 /opt/app/kafka
Extract the archive on each Kafka server. Kafka requires JDK 1.8+.
Configure Kafka brokers
Edit config/server.properties on each Kafka node:
- broker.id: must be unique across the cluster (an integer). Use 0 for the first node, 1 for the second, 2 for the third, etc.
- listeners and advertised.listeners: configure the listening address and the address advertised to clients. For a single machine, use the default PLAINTEXT://:9092. For multi-network or container environments, set advertised.listeners to the actual reachable address, e.g., PLAINTEXT://10.201.X.X:9092.
- zookeeper.connect: the Zookeeper cluster addresses and port:
zookeeper.connect=10.201.X.X:2181,10.201.X.X:2181,10.201.X.X:2181
If SolrCloud uses a /solr_demo subpath in Zookeeper, Kafka can connect to the root directory directly; Kafka will create /brokers and other paths in Zookeeper as needed.
- log.dirs: the directory where Kafka stores messages, e.g., /opt/app/kafka/logs.
Adjust Kafka memory and other parameters as needed, but defaults are adequate for initial testing.
Start Kafka
Start each Kafka broker in sequence:
$ ./bin/kafka-server-start.sh -daemon config/server.properties
The -daemon flag runs the process in the background. After startup, Kafka registers itself with Zookeeper. You can verify this by checking the /brokers/ids node in Zookeeper with a Zookeeper client.
Create topics
Create Kafka topics to carry product data updates. For example, create a topic named product_update with replication factor 2 and 3 partitions:
$ ./bin/kafka-topics.sh --create --topic product_update --partitions 3 --replication-factor 2 --bootstrap-server 10.201.X.X:9092
(--bootstrap-server specifies any Kafka node address). Verify creation succeeded by listing current topics:
$ ./bin/kafka-topics.sh --list --bootstrap-server 10.201.X.X:9092
The Kafka cluster is now ready.
Installing and Configuring SolrCloud
Solr is the core search indexing component. We use SolrCloud mode to support sharding and high availability (this example uses Solr 7.7.3).
Install Solr
Download Solr (zip or tgz format) and extract on each node:
$ wget https://archive.apache.org/dist/lucene/solr/7.7.3/solr-7.7.3.tgz
$ tar -zxvf solr-7.7.3.tgz
$ mv solr-7.7.3 /opt/app/solr
Prepare SolrCloud configuration
Before starting SolrCloud, upload the Solr core configuration to Zookeeper or let Solr auto-upload on startup. This example uses a collection named "product" with schema and Data Import Handler (DIH) configurations prepared in advance. Follow these steps:
- Configure ZK_HOST: edit solr-7.7.3/bin/solr.in.sh, locate ZK_HOST, and set it to your Zookeeper cluster address and the Solr configuration path:
ZK_HOST="10.201.X.X:2181,10.201.X.X:2181,10.201.X.X:2181/solr_demo"
The /solr_demo path means SolrCloud will store configuration data under that path in Zookeeper. Ensure this path exists (create it earlier with zkCli).
Cluster configuration: optionally adjust JVM heap size (SOLR_HEAP) and garbage collection policy in solr.in.sh. Solr 7 provides reasonable defaults; adjust if needed, e.g., SOLR_HEAP="1g".
DataImportHandler configuration: if using DIH to import data from Cassandra, add the DIH plugin to Solr. Solr 7.7.3 includes solr-dataimporthandler jar files but does not include them in the default solr-webapp. Copy the following to solr-webapp/webapp/WEB-INF/lib/:
- dist/solr-dataimporthandler-7.7.3.jar
- dist/solr-dataimporthandler-extras-7.7.3.jar
- (If your project provides a custom DIH handler like solr-support-7.7.0-1.0.jar, copy it as well)
Complete this preparation on one machine, then package the solr directory as solr.zip and distribute to all Solr servers to ensure consistency.
Upload Solr configuration and start SolrCloud
Assume the development team provided Solr core configurations (schema.xml, solrconfig.xml, data-config.xml, etc.) in /home/netty/solr_core_config/product. Upload this to Zookeeper:
$ cd /opt/app/solr
$ ./bin/solr zk upconfig -z 10.201.X.X:2181,10.201.X.X:2181,10.201.X.X:2181/solr_demo \
-n product -d /home/netty/solr_core_config/product
This command uploads the local product configuration directory to Zookeeper, registering it as the "product" config set. (To view existing configs, use downconfig, e.g., ./bin/solr zk downconfig -n product -d ./downloaded_conf -z ...).
Start Solr nodes
On each Solr server, start Solr in cloud mode to join the cluster:
$ ./bin/solr start -c -m 1g -z 10.201.X.X:2181,10.201.X.X:2181,10.201.X.X:2181/solr_demo -p 8983 -d /opt/app/solr/solr_data
Parameter meanings: -c enables cloud mode; -m 1g sets max heap to 1GB; -z specifies the Zookeeper address list and path; -p 8983 sets the listening port (default); -d /opt/app/solr/solr_data specifies the Solr instance data directory (we copy the example/server contents here for independent configuration).
After starting all Solr nodes, they automatically register with Zookeeper and form a cluster. Next, create a collection via the Solr API using the bin/solr script or Solr Admin UI:
$ ./bin/solr create -c product -n product -shards 2 -replicationFactor 2 -p 8983
This creates a collection named "product" using the "product" config set uploaded earlier, with 2 shards and 2 replicas per shard. The script distributes cores across nodes, and SolrCloud begins serving search requests. Verify by opening a browser to any Solr node's admin interface, e.g., http://10.201.X.X:8983/solr, to view cluster status and collection lists.
Note: SolrCloud relies on Zookeeper for configuration and state. Ensure your Zookeeper cluster is stable. Incorrect ZK settings (wrong address or path) will prevent Solr from starting.
Python Microservices Development and Deployment
With data storage and indexing in place, Python microservices connect these components. The original Java/Spring Boot modules handled data synchronization and search; we reimplement these with Python examples.
Microservice responsibilities
Data sync service (Kafka consumer): continuously consumes the Kafka product_update topic. When product data updates arrive, the service writes new data to Cassandra using the DataStax cassandra-driver and then triggers Solr index updates. Index updates may be incremental (via Solr HTTP API) or batch imports (via DIH), depending on strategy. Python can use kafka-python or confluent-kafka to consume messages, cassandra-driver to write to Cassandra, and requests or pysolr to call Solr APIs.
Search API service: exposes a search REST interface. Client requests are received and forwarded to SolrCloud, results are formatted and returned. Implement using Flask or FastAPI. Internally, use pysolr or direct HTTP requests to query Solr, then convert results to JSON. This service corresponds to the original Java search query module and can also supplement results from Cassandra—for example, retrieve product IDs from the search index and then query Cassandra for the latest stock levels if not indexed.
Supporting services: bulk import (like product-upload) or job triggering (like job-trigger) can also be implemented in Python. If these services need to integrate with existing scheduling frameworks (e.g., xxl-job), call their HTTP endpoints or implement the protocol.
Microservice code and startup scripts
Build a search API service with FastAPI and a Python startup script. Assume the search service code is in app.py (simplified example):
# app.py (FastAPI 简易示例)
from fastapi import FastAPI
import pysolr
solr = pysolr.Solr('http://10.201.X.X:8983/solr/product', timeout=10) # Solr 地址
app = FastAPI()
@app.get("/search")
def search(q: str):
# 在 Solr 中查询
results = solr.search(q)
# 提取需要的字段返回
docs = [doc for doc in results]
return {"query": q, "results": docs}
Production may require more complex query construction and result processing. Next, write a management script service_control.py to start or stop the FastAPI service. Use subprocess to invoke Uvicorn, mimicking traditional Shell scripts with java -jar ... &:
#!/usr/bin/env python3
# service_control.py
import subprocess, sys, os, signal
APP_COMMAND = ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7220"]
def start():
"""启动服务"""
# 将输出重定向到日志文件
logfile = open("service.log", "a")
# 使用 nohup & 类似效果启动子进程
process = subprocess.Popen(APP_COMMAND, stdout=logfile, stderr=logfile, preexec_fn=os.setpgrp)
print(f"Service started with PID {process.pid}")
def stop():
"""停止服务"""
# 查找运行中的进程(通过端口或命令名)
try:
# 利用 pgrep 查找 uvicorn 进程
result = subprocess.run(["pgrep", "-f", "uvicorn.*7220"], capture_output=True, text=True)
pids = result.stdout.strip().split()
if not pids:
print("Service is not running.")
return
for pid in pids:
os.kill(int(pid), signal.SIGTERM)
print("Service stopped.")
except Exception as e:
print(f"Error stopping service: {e}")
def restart():
"""重启服务"""
stop()
start()
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: service_control.py [start|stop|restart]")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "start":
start()
elif cmd == "stop":
stop()
elif cmd == "restart":
restart()
else:
print("Unknown command:", cmd)
This script mirrors Java service startup logic:
- Uvicorn runs the FastAPI app on port 7220 (simulating the original Java service port).
- start() uses subprocess.Popen to launch a background process in its own process group (equivalent to nohup). Log output goes to service.log.
- stop() uses pgrep to find processes matching uvicorn and the port, then sends SIGTERM to shut them down (alternatively, record the process ID in a file or use psutil to locate processes).
- restart() calls stop then start in sequence.
Place service_control.py on the server with execute permissions, then use ./service_control.py start to launch the Python microservice or ./service_control.py stop to stop it. For the data sync service (Kafka consumer), create a similar startup approach—write a consumer.py script and manage it with a control script.
Note: For production, use more robust hosting methods (Supervisor, systemd, or container orchestration). The scripts above are examples only.
Cluster Deployment Considerations
With all components deployed, pay attention to these cluster setup details and best practices:
- Configuration management and consistency: cluster configuration is complex and error-prone. Use configuration management tools (Ansible, Chef) or scripts to ensure consistency across machines. Parameters in solr.in.sh and cassandra.yaml must match across nodes. Custom configuration files (e.g., Solr schema) must be correctly versioned, uploaded to Zookeeper, and applied on all Solr nodes.
- Resource allocation: Cassandra, Solr, and Kafka are memory and I/O intensive. Adjust JVM heap settings based on hardware:
- Cassandra defaults to half physical RAM; in production, scale based on data volume but avoid excessive heap (long GC pauses).
- Solr heap depends on index data size; ensure the working set fits in memory. If using sorting or aggregation, increase heap. Place indices on local disk with sufficient space and good I/O performance.
- Kafka favors sequential writes to the filesystem. Ensure the log directory has adequate space and reserve memory for JVM heap and OS page cache. Kafka itself typically needs only a few GB of heap; performance relies more on OS caching.
- Network and ports: firewall rules must allow:
- Cassandra: 9042 (CQL), 7000/7001 (cluster communication), 7199 (JMX).
- Zookeeper: 2181 (clients), 2888/3888 (cluster internal) over the internal network.
- Kafka: 9092 (or custom listeners port) for clients; if spanning datacenters or containers, set advertised.listeners to the reachable address.
- Solr: 8983 (default) and internal node communication. If behind a firewall, expose the query port and Zookeeper port.
- Data import and consistency: on first deployment, load existing product data into Cassandra and Solr. Write batch scripts to read from the source (CSV, database) into Cassandra, then use Solr DIH for bulk import or the Solr API for batch indexing. Subsequently, incremental updates flow through Kafka → consumer → Cassandra/Solr. Ensure transactional consistency: write Cassandra first, then Solr; implement compensation if Solr fails (e.g., periodic full sync). Cassandra follows eventual consistency, so writes are final with no rollback; the application layer must handle failures gracefully.
- Monitoring and logging: after deployment, establish monitoring for each component:
- Cassandra: track node latency, read/write throughput, compaction status; scale or adjust parameters as needed.
- Solr: monitor query QPS, index size, cache hit rates via API or JMX.
- Kafka: watch message lag, consumer group status, disk usage.
- Python microservices: log critical operations (messages consumed, query latency) and use a process manager to restart on failure. Register with Supervisor or systemd.
- Failure testing: before production, test fault tolerance. Stop a Cassandra node and verify queries succeed (accounting for consistency level in the app). Stop a Solr node and verify queries switch to replicas. Verify Kafka remains operational if one broker goes down. Once validated, deploy with confidence.
Summary and Lessons Learned
This implementation built a distributed search system using Cassandra, SolrCloud, Zookeeper, Kafka, and Python microservices. Compared to monolithic search applications, this architecture offers scalability and modularity: Cassandra provides high write throughput and horizontal scale, SolrCloud offers powerful indexing, Kafka decouples data flow asynchronously, and Python microservices enable flexible business logic.
Key takeaways from this work:
- Thoughtful architecture: when introducing multiple components, define their roles and data flow clearly. In this example, Kafka decouples synchronization, making the system loosely coupled and scalable. For systems with infrequent data changes, direct application-triggered Solr updates may suffice, but a message queue improves long-term flexibility.
- Thorough testing: cluster configurations are intricate. After deployment, test repeatedly. We encountered issues like Solr nodes failing to join the cluster (traced to incorrect ZK paths) and high Kafka consumer lag (due to slow processing). Systematic debugging and adjustment—modifying configuration, optimizing code—resolved these problems.
- Python in place of Java: rewriting microservices in Python reduced boilerplate and compilation time, improving development velocity. However, account for Python's threading and multiprocessing semantics; leverage async I/O or multiprocessing to maximize performance. For performance-critical workloads, carefully evaluate Python's cost (Cython or JNI wrappers may help). In our case, Python handled data consumption and search APIs well with good maintainability.
- Configuration and operations: centralized configuration management is key to operational success. Version-control configuration files and use configuration management tools for batch deployment. For sensitive data (passwords, keys), use encryption or a configuration service. Maintain the system with regular tasks: Cassandra compaction, Solr index optimization, Kafka log cleanup to ensure stable long-term operation.
This deployment demonstrates a complete search solution. Building such a system from scratch requires knowledge across multiple technologies. Through step-by-step installation, configuration, and tuning, we mastered how each component integrates. Future enhancements might containerize this architecture on Kubernetes for automated scaling and operations. We hope this experience proves useful for engineers building similar search platforms.